diff --git a/.gitignore b/.gitignore index 3c23109d4c..1d837efd30 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,11 @@ local.properties ############# .idea/ +############# +## Sphinx +############# + +build/ ############# ## OS detritus @@ -109,3 +114,9 @@ $RECYCLE.BIN/ # Mac crap .DS_Store + +############# +## Gedit +############# + +*.*~ diff --git a/.travis.yml b/.travis.yml index d13ecc88e9..be72686450 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,11 +21,18 @@ install: - sudo apt-get update - sudo echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections - sudo apt-get install ttf-mscorefonts-installer + - sudo apt-get install unzip - sudo apt-get install texlive - sudo apt-get install texlive-xetex - sudo apt-get install texlive-lang-french - sudo apt-get install texlive-latex-extra + # Add fonts + - sudo wget -P /usr/share/fonts/truetype https://www.dropbox.com/s/ema28tjn52960mq/Merriweather.zip + - sudo unzip /usr/share/fonts/truetype/Merriweather.zip -d /usr/share/fonts/truetype/Merriweather/ + - sudo chmod a+r /usr/share/fonts/truetype/Merriweather/*.ttf + - sudo fc-cache -f -v + # Cabal + Pandoc stuff - sudo mkdir -p ~/cabal/bin - sudo mkdir -p ~/.pandoc @@ -53,6 +60,7 @@ install: script: - npm run-script travis - coverage run --source='.' manage.py test + - flake8 --exclude=migrations,urls.py,settings.py --max-line-length=120 zds after_success: - coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2a5797bd6..51c92f4349 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,16 +15,22 @@ Les contributions externes sont les bienvenues ! 2. Faites vos modifications 3. Ajoutez un test pour votre modification. Seules les modifications de documentation et les réusinages n'ont pas besoin de nouveaux tests 4. Assurez-vous que l'intégralité des tests passent : `python manage.py test` -5. Poussez votre travail et faites une _pull request_ +5. Assurez-vous que le code suit la [PEP-8](http://legacy.python.org/dev/peps/pep-0008/) : `flake8 --exclude=migrations,urls.py,settings.py --max-line-length=120 zds` +6. Si vous avez fait des modifications du _front_, jouez les tests associés : `gulp test` +7. Si vous modifiez le modèle (les fichiers models.py), n'oubliez pas de créer les fichiers de migration : `python manage.py schemamigration app_name --auto` +8. Poussez votre travail et faites une _pull request_ # Quelques bonnes pratiques * Respectez [les conventions de code de Django](https://docs.djangoproject.com/en/1.6/internals/contributing/writing-code/coding-style/), ce qui inclut la [PEP 8 de Python](http://legacy.python.org/dev/peps/pep-0008/) * Le code et les commentaires sont en anglais -* Le _workflow_ Git utilisé est le [git flow](http://nvie.com/posts/a-successful-git-branching-model/) à quelques détails près : - * Les contributions se font uniquement sur la branche `dev` (appelée `develop` dans le git flow standard) - * Lorsqu'on décide que `dev` est prête pour la prod, la branche est mergée dans `master` (les branches `releases` dans le git flow standard). Dès lors, cette branche ne reçoit plus que des corrections, aucune nouvelle fonctionnalité. +* Le _workflow_ Git utilisé est le [Git flow](http://nvie.com/posts/a-successful-git-branching-model/). En détail : + * Les arrivées fonctionnalités et corrections de gros bugs hors release se font via des PR. + * Ces PR sont unitaires. Aucune PR qui corrige plusieurs problèmes ou apporte plusieurs fonctionnalité ne sera accepté ; la règle est : une fonctionnalité ou une correction = une PR. + * Ces PR sont mergées dans la branche `dev` (appelée `develop` dans le git flow standard), après une QA légère. * Pensez à préfixer vos branches selon l'objet de votre PR : `hotfix-XXX`, `feature-XXX`, etc. * La branche `prod` (appelée `master` dans le git flow standard) contient exclusivement le code en production, pas la peine d'essayer de faire le moindre _commit_ dessus ! + +Tous les détails sur le workflow se trouvent [sur la page dédiée](doc/workflow.md). * Votre test doit échouer sans votre modification, et réussir avec * Faites des messages de _commit_ clairs et en français @@ -41,9 +47,10 @@ Les contributions externes sont les bienvenues ! | Nouvelle Fonctionnalité ? | [oui|non] | Tickets concernés | [Liste de tickets séparés par des virgules] ``` +* Ajoutez des notes de QA (Quality Assurance). Ces notes doivent permettent à un testeur de comprendre ce que vous avez modifié, ce qu'il faut tester en priorité et les pièges auxquels il doit s'attendre et donc sur lesquels porter une attention particulière. Précisez tout particulièrement s'il est nécéssaire d'effectuer une action de gestion préalable, comme `python manage.py migrate`, `python manage.py loaddata fixture/*.yaml` ou `gulp build`. ## Les commits -* Pour les commits, nous suivons le même ordre d'idée des standards git, à savoir : +* Pour les commits, nous suivons le même ordre d'idée des standards Git, à savoir : * La première ligne du commit ne doit pas faire plus de 50 caractères * Si besoin, complétez votre commit via des commentaires, en respectant une limite de 70 caractères par ligne * Bien que le code soit en anglais, le commit doit être de préférence en français diff --git a/Gulpfile.js b/Gulpfile.js index 3231b43069..72b06248bd 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -5,7 +5,7 @@ var gulp = require("gulp"), mainBowerFiles = require('main-bower-files'); var paths = { - scripts: "assets/js/**", + scripts: ["assets/js/**", "!assets/js/_**"], images: "assets/images/**", smileys: "assets/smileys/**", errors_main: "errors/scss/main.scss", @@ -25,9 +25,11 @@ var paths = { sprite: "assets/images/sprite@2x/*.png" }; + + gulp.task("clean", function() { - return gulp.src(["dist/*"]) - .pipe($.clean()); + return gulp.src(["dist/*"], { read: false }) + .pipe($.rimraf()); }); gulp.task("script", ["test"], function() { @@ -43,8 +45,8 @@ gulp.task("script", ["test"], function() { }); gulp.task("clean-errors", function() { - return gulp.src(["errors/css/*"]) - .pipe($.clean()); + return gulp.src(["errors/css/*"], { read: false }) + .pipe($.rimraf()); }); gulp.task("errors", ["clean-errors"], function() { @@ -95,7 +97,9 @@ gulp.task("sprite", function() { return output; } })); - sprite.img.pipe(gulp.dest("dist/images")); + sprite.img + .pipe($.imagemin({ optimisationLevel: 3, progressive: true, interlaced: true })) + .pipe(gulp.dest("dist/images")); sprite.css.pipe(gulp.dest(paths.styles.sass)); return sprite.css; }); @@ -117,11 +121,13 @@ gulp.task("smileys", function() { }); gulp.task("vendors", function() { - return gulp.src(mainBowerFiles()) + var vendors = mainBowerFiles(); + vendors.push("assets/js/_**"); + + return gulp.src(vendors) .pipe($.newer("dist/js/vendors.js")) .pipe($.flatten()) // remove folder structure .pipe($.size({ title: "vendors", showFiles: true })) - .pipe(gulp.dest("dist/js/vendors")) .pipe($.concat("vendors.js")) .pipe($.size({ title: "vendors.js" })) .pipe(gulp.dest("dist/js")) @@ -144,7 +150,7 @@ gulp.task("watch", function(cb) { gulp.watch(paths.images, ["images"]); gulp.watch(paths.styles_path, ["stylesheet"]); gulp.watch(paths.errors_path, ["errors"]); - gulp.watch(paths.sprite, ["sprite", "stylesheet"]); + gulp.watch(paths.sprite, ["stylesheet"]); // stylesheet task already lauch sprite gulp.watch("dist/*/**", function(file) { var filePath = path.join("static/", path.relative(path.join(__dirname, "dist/"), file.path)); // Pour que le chemin ressemble à static/.../... @@ -172,9 +178,9 @@ gulp.task("pack", ["build"], function() { .pipe(gulp.dest("dist/")); }); -gulp.task("travis", ["test"]); +gulp.task("travis", ["pack"]); -gulp.task("build", ["smileys", "images", "sprite", "stylesheet", "vendors", "script", "merge-scripts"]); +gulp.task("build", ["smileys", "images", "sprite", "stylesheet", "merge-scripts"]); gulp.task("default", ["build", "watch"]); diff --git a/README.md b/README.md index 31e1f0990e..f567bd0b43 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,35 @@ [![Build Status](https://travis-ci.org/zestedesavoir/zds-site.svg?branch=dev)](https://travis-ci.org/zestedesavoir/zds-site) [![Coverage Status](https://coveralls.io/repos/zestedesavoir/zds-site/badge.png?branch=dev)](https://coveralls.io/r/zestedesavoir/zds-site?branch=dev) -[![Licnce GPL](http://img.shields.io/badge/license-GPL-yellow.svg)](http://www.gnu.org/licenses/quick-guide-gplv3.fr.html) +[![Licence GPL](http://img.shields.io/badge/license-GPL-yellow.svg)](http://www.gnu.org/licenses/quick-guide-gplv3.fr.html) +[![Documentation Status](https://readthedocs.org/projects/zds-site/badge/?version=latest)](https://readthedocs.org/projects/zds-site/?badge=latest) - - - - -Zeste de Savoir -=============== +# Zeste de Savoir Site internet communautaire codé à l'aide du framework [Django](https://www.djangoproject.com/) 1.6 et de [Python](https://www.djangoproject.com/) 2.7. * Lien du site : [zestedesavoir](http://www.zestedesavoir.com) +## Fonctionnalités implementées - - - -Fonctionnalités implementées ----------------------------- - -- Membres - Tutoriels - Articles +- Membres - Forums - Messages privés - Galeries d'images - Recherche - - - - -Fonctionnalités à venir ------------------------ +## Fonctionnalités à venir Elles sont reportées essentiellement dans le [bugtraker](https://github.com/zestedesavoir/zds-site/issues). - - - - -Comment démarrer une instance de ZdS ? --------------------------------------- - +## Comment démarrer une instance de ZdS ? ### Installation d'une version locale de ZdS - [Intallation sur Windows](doc/install-windows.md) - [Intallation sur Linux](doc/install-linux.md) - [Intallation sur OS X](doc/install-os-x.md) - +- [Installation de Solr](doc/install-solr.md) pour gérer la recherche ### Mettre à jour votre version locale de ZdS Après avoir mis à jour votre dépot, vous devez executer les commandes suivantes (depuis la racine de votre projet) pour mettre à jour les dépendances. @@ -59,21 +39,23 @@ python manage.py migrate pip install --upgrade -r requirements.txt ``` - ### Données de test Pour bénéficier de données de test, exécutez les commandes suivantes, dans l'ordre, à la fin des précédentes : ```console -python manage.py loaddata fixtures/users.yaml fixtures/forums.yaml fixtures/topics.yaml fixtures/mps.yaml fixtures/categories.yaml fixtures/licences.yaml +python manage.py loaddata fixtures/*.yaml ``` Cela va créer plusieurs entitées : -* 3 utilisateurs (utilisateur/mot de passe) : +* 6 utilisateurs (utilisateur/mot de passe) : * user/user : Utilisateur normal * staff/staff : Utilisateur avec les droits d'un staff * admin/admin : Utilisateur avec les droits d'un staff et d'un admin + * anonymous/anonymous : Utilisateur qui permet l'anonymisation des messages sur les forums + * Auteur externe/external : Utilisateur qui permet de récupérer les tutoriels d'anciens membres et/ou de publier des tutoriels externes. + * ïtrema/ïtrema : Utilisateur de test supplémentaire sans droit * 3 catégories * 11 forums * 3 sujets avec une réponse @@ -81,44 +63,24 @@ Cela va créer plusieurs entitées : * 3 catégories et 2 sous-catégories -### Conseils de developpement - -Avant de faire une Pull Request (PR), vérifiez que votre code passe tous les tests unitaires et qu'il est compatible [PEP-8](http://legacy.python.org/dev/peps/pep-0008/) en exécutant les commandes suivantes, pour le back : - -```console -python manage.py test -flake8 --exclude=migrations,urls.py --max-line-length=120 --ignore=F403,E126,E127,E128 zds -``` +### La documentation complète -Pour le front : +En ligne : http://zds-site.readthedocs.org/ -```console -gulp test -``` +La documentation de ZdS est générée par Sphinx, et elle doit être mise à jour à chaque modification ou ajout d'une fonctionnalité du site. Les sources se trouvent [ici](doc/sphinx/source/) -Si vous modifiez le modèle (les fichiers models.py), n'oubliez pas de créer les fichiers de migration : - -```console -python manage.py schemamigration app_name --auto -``` - -Si vous avez une connexion lente et que vous ne voulez travailler que sur une branche précise, vous pouvez toujours ne récupérer que celle-ci : - -```console -git clone https://github.com/zestedesavoir/zds-site.git -b LA_BRANCHE --depth 1 -``` +Pour générer la documentation en local, rendez vous dans le répertoire `zds-site/doc/sphinx` depuis votre terminal, et lancez la commande `make html`. Vous pourrez ensuite la consulter en ouvrant le fichier `zds-site/doc/sphinx/build/html/index.html` +### Conseils de developpement +Vous trouverez tout sur [la page dédiée de la documentation](CONTRIBUTING.md) -En savoir plus --------------- +## En savoir plus - [Comment déployer ZDS sur un serveur de production ?](doc/deploy.md) -- [Contribuer](CONTRIBUTING.md) +- [Comment contribuer et conseils de développement](CONTRIBUTING.md) - [Comment contribuer : comprendre comment suivre le workflow (sur zds)](http://zestedesavoir.com/forums/sujet/324/comment-contribuer-comprendre-comment-suivre-le-workflow/) - - - +- [Les détails du workflow utilisé sur Zeste de Savoir](doc/workflow.md) Zeste de Savoir est basé sur un fork de [Progdupeu.pl](http://progdupeu.pl) ([Dépôt Bitbucket](https://bitbucket.org/MicroJoe/progdupeupl/)). diff --git a/assets/images/logo.png b/assets/images/logo.png index 2261b0d959..a2d2e382b2 100644 Binary files a/assets/images/logo.png and b/assets/images/logo.png differ diff --git a/assets/images/logo@2x.png b/assets/images/logo@2x.png index 4a2b8b36d5..d3324280e6 100644 Binary files a/assets/images/logo@2x.png and b/assets/images/logo@2x.png differ diff --git a/assets/images/sprite@2x/email-blue.png b/assets/images/sprite@2x/email-blue.png new file mode 100644 index 0000000000..ef488e9344 Binary files /dev/null and b/assets/images/sprite@2x/email-blue.png differ diff --git a/assets/images/sprite@2x/email-light.png b/assets/images/sprite@2x/email-light.png new file mode 100644 index 0000000000..46ea636f9e Binary files /dev/null and b/assets/images/sprite@2x/email-light.png differ diff --git a/assets/images/sprite@2x/email.png b/assets/images/sprite@2x/email.png new file mode 100644 index 0000000000..80a111400d Binary files /dev/null and b/assets/images/sprite@2x/email.png differ diff --git a/assets/images/sprite@2x/facebook-blue.png b/assets/images/sprite@2x/facebook-blue.png new file mode 100644 index 0000000000..cb6b7cd95b Binary files /dev/null and b/assets/images/sprite@2x/facebook-blue.png differ diff --git a/assets/images/sprite@2x/facebook-light.png b/assets/images/sprite@2x/facebook-light.png new file mode 100644 index 0000000000..d2a33a10ae Binary files /dev/null and b/assets/images/sprite@2x/facebook-light.png differ diff --git a/assets/images/sprite@2x/facebook.png b/assets/images/sprite@2x/facebook.png new file mode 100644 index 0000000000..034d7464b9 Binary files /dev/null and b/assets/images/sprite@2x/facebook.png differ diff --git a/assets/images/sprite@2x/foursquare-blue.png b/assets/images/sprite@2x/foursquare-blue.png new file mode 100644 index 0000000000..d1a6cc4403 Binary files /dev/null and b/assets/images/sprite@2x/foursquare-blue.png differ diff --git a/assets/images/sprite@2x/foursquare-light.png b/assets/images/sprite@2x/foursquare-light.png new file mode 100644 index 0000000000..05ae9a6174 Binary files /dev/null and b/assets/images/sprite@2x/foursquare-light.png differ diff --git a/assets/images/sprite@2x/foursquare.png b/assets/images/sprite@2x/foursquare.png new file mode 100644 index 0000000000..3d56f13719 Binary files /dev/null and b/assets/images/sprite@2x/foursquare.png differ diff --git a/assets/images/sprite@2x/github-blue.png b/assets/images/sprite@2x/github-blue.png new file mode 100644 index 0000000000..4309170469 Binary files /dev/null and b/assets/images/sprite@2x/github-blue.png differ diff --git a/assets/images/sprite@2x/github-light.png b/assets/images/sprite@2x/github-light.png new file mode 100644 index 0000000000..d501deb6b6 Binary files /dev/null and b/assets/images/sprite@2x/github-light.png differ diff --git a/assets/images/sprite@2x/github.png b/assets/images/sprite@2x/github.png new file mode 100644 index 0000000000..27c86b9b63 Binary files /dev/null and b/assets/images/sprite@2x/github.png differ diff --git a/assets/images/sprite@2x/google-plus-blue.png b/assets/images/sprite@2x/google-plus-blue.png new file mode 100644 index 0000000000..273d5b98f3 Binary files /dev/null and b/assets/images/sprite@2x/google-plus-blue.png differ diff --git a/assets/images/sprite@2x/google-plus-light.png b/assets/images/sprite@2x/google-plus-light.png new file mode 100644 index 0000000000..44d991a91e Binary files /dev/null and b/assets/images/sprite@2x/google-plus-light.png differ diff --git a/assets/images/sprite@2x/google-plus.png b/assets/images/sprite@2x/google-plus.png new file mode 100644 index 0000000000..02934a67f3 Binary files /dev/null and b/assets/images/sprite@2x/google-plus.png differ diff --git a/assets/images/sprite@2x/help-blue.png b/assets/images/sprite@2x/help-blue.png new file mode 100644 index 0000000000..46f45c4fec Binary files /dev/null and b/assets/images/sprite@2x/help-blue.png differ diff --git a/assets/images/sprite@2x/help-light.png b/assets/images/sprite@2x/help-light.png new file mode 100644 index 0000000000..8644741815 Binary files /dev/null and b/assets/images/sprite@2x/help-light.png differ diff --git a/assets/images/sprite@2x/help.png b/assets/images/sprite@2x/help.png new file mode 100644 index 0000000000..5d60f20e1b Binary files /dev/null and b/assets/images/sprite@2x/help.png differ diff --git a/assets/images/sprite@2x/hide-blue.png b/assets/images/sprite@2x/hide-blue.png new file mode 100644 index 0000000000..0e2c262de1 Binary files /dev/null and b/assets/images/sprite@2x/hide-blue.png differ diff --git a/assets/images/sprite@2x/hide-light.png b/assets/images/sprite@2x/hide-light.png new file mode 100644 index 0000000000..241dce561e Binary files /dev/null and b/assets/images/sprite@2x/hide-light.png differ diff --git a/assets/images/sprite@2x/hide.png b/assets/images/sprite@2x/hide.png new file mode 100644 index 0000000000..cef7e6b4cd Binary files /dev/null and b/assets/images/sprite@2x/hide.png differ diff --git a/assets/images/sprite@2x/twitter-blue.png b/assets/images/sprite@2x/twitter-blue.png new file mode 100644 index 0000000000..08a2b4a88a Binary files /dev/null and b/assets/images/sprite@2x/twitter-blue.png differ diff --git a/assets/images/sprite@2x/twitter-light.png b/assets/images/sprite@2x/twitter-light.png new file mode 100644 index 0000000000..9cecf6206d Binary files /dev/null and b/assets/images/sprite@2x/twitter-light.png differ diff --git a/assets/images/sprite@2x/twitter.png b/assets/images/sprite@2x/twitter.png new file mode 100644 index 0000000000..904415d1e6 Binary files /dev/null and b/assets/images/sprite@2x/twitter.png differ diff --git a/assets/images/teasing/logo.png b/assets/images/teasing/logo.png deleted file mode 100644 index 31bb30cfe2..0000000000 Binary files a/assets/images/teasing/logo.png and /dev/null differ diff --git a/assets/images/teasing/social.jpg b/assets/images/teasing/social.jpg deleted file mode 100644 index 3161ff320b..0000000000 Binary files a/assets/images/teasing/social.jpg and /dev/null differ diff --git a/assets/js/_custom.modernizr.js b/assets/js/_custom.modernizr.js new file mode 100644 index 0000000000..db62480ec8 --- /dev/null +++ b/assets/js/_custom.modernizr.js @@ -0,0 +1,4 @@ +/* Modernizr 2.8.3 (Custom Build) | MIT & BSD + * Build: http://modernizr.com/download/#-flexboxlegacy-inlinesvg-svg-svgclippaths-touch-shiv-mq-cssclasses-teststyles-testallprops-prefixes-css_mask-ie8compat + */ +;window.Modernizr=function(a,b,c){function B(a){j.cssText=a}function C(a,b){return B(m.join(a+";")+(b||""))}function D(a,b){return typeof a===b}function E(a,b){return!!~(""+a).indexOf(b)}function F(a,b){for(var d in a){var e=a[d];if(!E(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function G(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:D(f,"function")?f.bind(d||b):f}return!1}function H(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+o.join(d+" ")+d).split(" ");return D(b,"string")||D(b,"undefined")?F(e,b):(e=(a+" "+p.join(d+" ")+d).split(" "),G(e,b,c))}var d="2.8.3",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n="Webkit Moz O ms",o=n.split(" "),p=n.toLowerCase().split(" "),q={svg:"http://www.w3.org/2000/svg"},r={},s={},t={},u=[],v=u.slice,w,x=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},y=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b)&&c(b).matches||!1;var d;return x("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},z={}.hasOwnProperty,A;!D(z,"undefined")&&!D(z.call,"undefined")?A=function(a,b){return z.call(a,b)}:A=function(a,b){return b in a&&D(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=v.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(v.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(v.call(arguments)))};return e}),r.flexboxlegacy=function(){return H("boxDirection")},r.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:x(["@media (",m.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},r.svg=function(){return!!b.createElementNS&&!!b.createElementNS(q.svg,"svg").createSVGRect},r.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==q.svg},r.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(l.call(b.createElementNS(q.svg,"clipPath")))};for(var I in r)A(r,I)&&(w=I.toLowerCase(),e[w]=r[I](),u.push((e[w]?"":"no-")+w));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)A(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},B(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=m,e._domPrefixes=p,e._cssomPrefixes=o,e.mq=y,e.testProp=function(a){return F([a])},e.testAllProps=H,e.testStyles=x,g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+u.join(" "):""),e}(this,this.document),Modernizr.addTest("cssmask",Modernizr.testAllProps("maskRepeat")),Modernizr.addTest("ie8compat",function(){return!window.addEventListener&&document.documentMode&&document.documentMode===7}); \ No newline at end of file diff --git a/assets/js/accessibility-links.js b/assets/js/accessibility-links.js index 9831e8c38d..1e2c3f7521 100644 --- a/assets/js/accessibility-links.js +++ b/assets/js/accessibility-links.js @@ -4,7 +4,7 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; $("#accessibility a").on("focus", function(){ diff --git a/assets/js/accordeon.js b/assets/js/accordeon.js index 9159c5f1d2..a905de46db 100644 --- a/assets/js/accordeon.js +++ b/assets/js/accordeon.js @@ -4,22 +4,30 @@ Author: Alex-D ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; - - $(".main .sidebar.accordeon, .main .sidebar .accordeon").each(function(){ - var $that = this; - $("h4 + ul, h4 + ol", $that).each(function(){ + function accordeon($elem){ + $("h4 + ul, h4 + ol", $elem).each(function(){ if($(".current", $(this)).length === 0) $(this).hide(); }); - $("h4", $that).click(function(e){ + $("h4", $elem).click(function(e){ $("+ ul, + ol", $(this)).slideToggle(100); e.preventDefault(); e.stopPropagation(); }); + } + + $(document).ready(function(){ + $(".main .sidebar.accordeon, .main .sidebar .accordeon") + .each(function(){ + accordeon($(this)); + }) + .on("DOMNodeInserted", function(e){ + accordeon($(e.target)); + }); }); })(jQuery); \ No newline at end of file diff --git a/assets/js/autocompletion.js b/assets/js/autocompletion.js index b5cd07715a..125ad1f8de 100644 --- a/assets/js/autocompletion.js +++ b/assets/js/autocompletion.js @@ -4,7 +4,7 @@ Author: Sandhose / Quentin Gliech ========================================================================== */ -(function($) { +(function($, undefined) { "use strict"; function AutoComplete(input, options) { @@ -118,7 +118,7 @@ selected = selected || this.selected; var input = this.$input.val(); var lastChar = input.substr(-1); - if((lastChar === "," || lastChar === " " || selected === -1) && this.options.type === "multiple") + if((lastChar === "," || selected === -1) && this.options.type === "multiple") return false; var completion = this.getFromCache(selected); @@ -126,9 +126,9 @@ return false; if(this.options.type === "multiple") { - var lastSpace = input.replace(",", " ").lastIndexOf(" "); - if(lastSpace){ - input = input.substr(0, lastSpace + 1) + completion.value + ", "; + var lastComma = input.lastIndexOf(","); + if(lastComma !== -1){ + input = input.substr(0, lastComma + 2) + completion.value + ", "; this.$input.val(input); } else { this.$input.val(completion.value + ", "); @@ -148,9 +148,9 @@ }, extractWords: function(input){ - input = input.replace(/ /g, ","); // Replace space with comas + //input = input.replace(/ /g, ","); // Replace space with comas var words = $.grep( - input.split(","), // Remove empty + $.map(input.split(","), $.trim), // Remove empty function(e){ return e === "" || e === undefined; }, @@ -162,8 +162,7 @@ parseInput: function(input){ if(this.options.type === "multiple") { - var lastChar = input.substr(-1); - if(lastChar === "," || lastChar === " ") + if(input.substr(-1) === "," || input.substr(-2) === ", ") return false; var words = this.extractWords(input); @@ -291,5 +290,10 @@ $(document).ready(function() { $("[data-autocomplete]").autocomplete(); + $("#content").on("DOMNodeInserted", "input", function(e){ + var $input = $(e.target); + if($input.is("[data-autocomplete]")) + $input.autocomplete(); + }); }); })(jQuery); \ No newline at end of file diff --git a/assets/js/close-alert-box.js b/assets/js/close-alert-box.js index 278b8d66f0..f6c10e094a 100644 --- a/assets/js/close-alert-box.js +++ b/assets/js/close-alert-box.js @@ -4,10 +4,10 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; - $(".close-alert-box:not(.open-modal)").on("click", function(e) { + $(".main").on("click", ".close-alert-box:not(.open-modal)", function(e) { $(this).parents(".alert-box:first").slideUp(150, function(){ $(this).remove(); }); diff --git a/assets/js/data-click.js b/assets/js/data-click.js index e3ab6e31b2..c823226bb0 100644 --- a/assets/js/data-click.js +++ b/assets/js/data-click.js @@ -4,26 +4,37 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, document, undefined){ "use strict"; var dropdownMouseDown = false; - - $("[data-click]") - .on("mousedown", function(){ - dropdownMouseDown = true; - }) - .on("mouseup", function(){ - dropdownMouseDown = false; - }) - .on("click focus", function(e){ - if(e.type === "focus" && dropdownMouseDown) - return false; - if(!($(this).hasClass("dont-click-if-sidebar") && $(".header-container .mobile-menu-btn").is(":visible"))){ - e.preventDefault(); - e.stopPropagation(); - $("#" + $(this).attr("data-click")).trigger("click"); - } + function dataClick($elem){ + $elem + .attr("tabindex", -1) + .attr("aria-hidden", true) + .on("mousedown", function(){ + dropdownMouseDown = true; + }) + .on("mouseup", function(){ + dropdownMouseDown = false; + }) + .on("click focus", function(e){ + if(e.type === "focus" && dropdownMouseDown) + return false; + + if(!($(this).hasClass("dont-click-if-sidebar") && $(".header-container .mobile-menu-btn").is(":visible"))){ + e.preventDefault(); + e.stopPropagation(); + $("#" + $(this).attr("data-click")).trigger(e.type); + } + }); + } + + $(document).ready(function(){ + dataClick($("[data-click]")); + $("#content").on("DOMNodeInserted", "[data-click]", function(e){ + dataClick($(e.target)); + }); }); -})(jQuery); \ No newline at end of file +})(jQuery, document); \ No newline at end of file diff --git a/assets/js/dropdown-menu.js b/assets/js/dropdown-menu.js index 3c76b65778..390dd0c635 100644 --- a/assets/js/dropdown-menu.js +++ b/assets/js/dropdown-menu.js @@ -4,23 +4,29 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; - var dropdownMouseDown = false; + var mouseDown = false, + shiftHold = false; + + $(document).on("keydown keyup", function(e){ + shiftHold = e.shiftKey; + }); $(".dropdown").each(function(){ - var $elem = $(this).parent().find("> a"); + var $dropdown = $(this), + $elem = $(this).parent().find("> a"); if(!$elem.parents(".logbox").length) $elem.addClass("has-dropdown"); $elem .on("mousedown", function(){ - dropdownMouseDown = true; + mouseDown = true; }) .on("mouseup", function(){ - dropdownMouseDown = false; + mouseDown = false; }) .on("click", function(e){ if(($(this).parents(".header-menu-list").length > 0 && parseInt($("html").css("width")) < 960)) @@ -40,18 +46,32 @@ .on("focus", function(e){ e.preventDefault(); - if(!dropdownMouseDown && !$(this).hasClass("active")){ - activeDropdown($(this)); + if(!mouseDown && !$elem.hasClass("active")){ + activeDropdown($elem); - $(this) - .one("blur", function(){ - $elem = $(this); + $elem + .off("blur") + .on("blur", function(){ + $elem + .one("blur", function(){ + if(shiftHold) + triggerCloseDropdown($elem); + }); + setTimeout(function(){ - if($(":tabbable:focus", $elem.parent().find(".dropdown")).length){ - $(":tabbable:last", $elem.parent().find(".dropdown")).one("blur", function(){ - $elem.removeClass("active"); - triggerCloseDropdown($elem); - }); + if($(":tabbable:focus", $dropdown).length){ + var listenBlurLast = function(){ + $(":tabbable:last", $dropdown) + .one("blur", function(){ + if(shiftHold){ + listenBlurLast(); + return; + } + $elem.removeClass("active"); + triggerCloseDropdown($elem); + }); + }; + listenBlurLast(); } else { $elem.removeClass("active"); triggerCloseDropdown($elem); diff --git a/assets/js/editor.js b/assets/js/editor.js index 163082846b..53846b3643 100644 --- a/assets/js/editor.js +++ b/assets/js/editor.js @@ -1,5 +1,5 @@ -(function(){ +(function(window, document, undefined){ "use strict"; var zForm = { @@ -101,15 +101,23 @@ for (var i=0, c=listTexta.length; i 0){ - var $navigableElems = $list.find(".navigable-elem"); - $("body").on("keydown", function(e){ - if(!$(document.activeElement).is(":input") && (e.which === 74 || e.which === 75)){ - var $current = $list.find(".navigable-elem.active"), - nextIndex = null; - - if($current.length === 1){ - var currentIndex = $navigableElems.index($current); - if(e.which === 75){ // J - if(currentIndex > 0) - nextIndex = currentIndex - 1; - } else { // K - if(currentIndex + 1 < $navigableElems.length) - nextIndex = currentIndex + 1; + $(document).ready(function(){ + var $lists = $("#content .navigable-list"); + + if($lists.length > 0){ + var $navigableElems = $lists.find(".navigable-elem"); + + $("body").on("keydown", function(e){ + if(!$(document.activeElement).is(":input") && (e.which === 74 || e.which === 75)){ + var $current = $lists.find(".navigable-elem.active"), + nextIndex = null; + + if($current.length === 1){ + var currentIndex = $navigableElems.index($current); + if(e.which === 75){ // J + if(currentIndex > 0) + nextIndex = currentIndex - 1; + } else { // K + if(currentIndex + 1 < $navigableElems.length) + nextIndex = currentIndex + 1; + } + } else { + nextIndex = 0; + } + + if(nextIndex !== null){ + $current.removeClass("active"); + activeNavigableElem($navigableElems.eq(nextIndex)); } - } else { - nextIndex = 0; } + }); - if(nextIndex !== null){ - $current.removeClass("active"); - activeNavigableElem($navigableElems.eq(nextIndex)); + $("#content .navigable-list") + .on("focus", ".navigable-link", function(){ + if(!$(this).parents(".navigable-elem:first").hasClass("active")){ + $lists.find(".navigable-elem.active").removeClass("active"); + activeNavigableElem($(this).parents(".navigable-elem")); } - } - }); + }) + .on("blur", ".navigable-link", function(){ + $(this).parents(".navigable-elem:first").removeClass("active"); + }); + } - $list.find(".navigable-link").on("focus", function(){ - if(!$(this).parents(".navigable-elem:first").hasClass("active")){ - $list.find(".navigable-elem.active").removeClass("active"); - activeNavigableElem($(this).parents(".navigable-elem")); - } - }); - $list.find(".navigable-link").on("blur", function(){ - $(this).parents(".navigable-elem:first").removeClass("active"); + function activeNavigableElem($elem){ + $elem + .addClass("active") + .find(".navigable-link") + .focus(); + } + + $("#content").on("DOMNodeInserted", ".navigable-list, .navigable-elem", function(){ + $lists = $("#content .navigable-list"); }); - } - - function activeNavigableElem($elem){ - $elem - .addClass("active") - .find(".navigable-link") - .focus(); - } -})(jQuery); \ No newline at end of file + }); +})(document, jQuery); \ No newline at end of file diff --git a/assets/js/markdown-help.js b/assets/js/markdown-help.js index b4c99682fd..baf8eed2d6 100644 --- a/assets/js/markdown-help.js +++ b/assets/js/markdown-help.js @@ -5,26 +5,36 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function(document ,$, undefined){ "use strict"; - - $(".md-editor").each(function(){ - var $help = $("
", { - "class": "markdown-help", - "html": "
" + - "

Les simples retours à la ligne ne sont pas pris en compte. Pour créer un nouveau paragraphe, pensez à sauter une ligne !

" + - "
**gras** \n*italique* \n[texte de lien](url du lien) \n> citation \n+ liste a puces 
" + - "Voir la documentation complète
" + - ""+ - "Masquer" + - "Afficher l'aide Markdown" + - "" + + function addDocMD($elem){ + $elem.each(function(){ + var $help = $("
", { + "class": "markdown-help", + "html": "
" + + "

Les simples retours à la ligne ne sont pas pris en compte. Pour créer un nouveau paragraphe, pensez à sauter une ligne !

" + + "
**gras** \n*italique* \n[texte de lien](url du lien) \n> citation \n+ liste a puces 
" + + "Voir la documentation complète
" + + ""+ + "Masquer" + + "Afficher l'aide Markdown" + + "" + }); + $(this).after($help); + $(".open-markdown-help, .close-markdown-help", $help).click(function(e){ + $(".markdown-help-more", $help).toggleClass("show-markdown-help"); + e.preventDefault(); + e.stopPropagation(); + }); }); - $(this).after($help); - $(".open-markdown-help, .close-markdown-help", $help).click(function(e){ - $(".markdown-help-more", $help).toggleClass("show-markdown-help"); - e.preventDefault(); - e.stopPropagation(); + } + + + $(document).ready(function(){ + addDocMD($(".md-editor")); + $("#content").on("DOMNodeInserted", ".md-editor", function(e){ + addDocMD($(e.target)); }); }); -})(jQuery); \ No newline at end of file +})(document, jQuery); \ No newline at end of file diff --git a/assets/js/message-hidden.js b/assets/js/message-hidden.js index 722b902b34..410685b02e 100644 --- a/assets/js/message-hidden.js +++ b/assets/js/message-hidden.js @@ -4,10 +4,11 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; - $("[href^=#show-message-hidden]").click(function(){ + $("#content [href^=#show-message-hidden]").on("click", function(e){ $(this).parents(".message:first").find(".message-hidden-content").toggle(); + e.preventDefault(); }); })(jQuery); \ No newline at end of file diff --git a/assets/js/mobile-menu.js b/assets/js/mobile-menu.js index 86938ffb3c..283f3b7d53 100644 --- a/assets/js/mobile-menu.js +++ b/assets/js/mobile-menu.js @@ -4,7 +4,7 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function(window, document, $, undefined){ "use strict"; /** @@ -314,4 +314,4 @@ } }); $(window).trigger("resize"); -})(jQuery); \ No newline at end of file +})(window, document, jQuery); \ No newline at end of file diff --git a/assets/js/modal.js b/assets/js/modal.js index 2d1e348203..126d4d3b6e 100644 --- a/assets/js/modal.js +++ b/assets/js/modal.js @@ -4,41 +4,44 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function(document, $, undefined){ "use strict"; var $overlay = $("
", { "id": "modals-overlay" }).on("click", function(e){ - closeModal($("#modals .modal:visible")); + closeModal(); e.preventDefault(); e.stopPropagation(); }); - $("body").append($("
", { "id": "modals" })); - $(".modal").each(function(){ - $("#modals").append($(this).addClass("tab-modalize")); - $(this).append($("", { - "class": "btn btn-cancel " + ($(this).is("[data-modal-close]") ? "btn-modal-fullwidth" : ""), - "href": "#close-modal", - "text": $(this).is("[data-modal-close]") ? $(this).attr("data-modal-close") : "Annuler" - }).on("click", function(e){ - closeModal(); - e.preventDefault(); - e.stopPropagation(); - })); - var $link = $("[href=#"+$(this).attr("id")+"]:first"); - var linkIco = $link.hasClass("ico-after") ? " light " + $link.attr("class").replace(/btn[a-z-]*/g, "") : ""; - $(this).prepend($("", { - "class": "modal-title" + linkIco, - "text": $link.text() - })); - }); - $("#modals").append($overlay); - + var $modals = $("
", { "id": "modals" }); + $("body").append($modals); + $modals.append($overlay); + function buildModals($elems){ + $elems.each(function(){ + $modals.append($(this).addClass("tab-modalize")); + $(this).append($("", { + class: "btn btn-cancel " + ($(this).is("[data-modal-close]") ? "btn-modal-fullwidth" : ""), + href: "#close-modal", + text: $(this).is("[data-modal-close]") ? $(this).attr("data-modal-close") : "Annuler", + click: function(e){ + closeModal(); + e.preventDefault(); + e.stopPropagation(); + } + })); + var $link = $("[href=#"+$(this).attr("id")+"]:first"); + var linkIco = $link.hasClass("ico-after") ? " light " + $link.attr("class").replace(/btn[a-z-]*/g, "") : ""; + $(this).prepend($("", { + class: "modal-title" + linkIco, + text: $link.text() + })); + }); + } - $(".open-modal").on("click", function(e){ + $("body").on("click", ".open-modal", function(e){ $overlay.show(); $($(this).attr("href")).show(0, function(){ $(this).find("input:visible, select, textarea").first().focus(); @@ -51,19 +54,24 @@ }); $("body").on("keydown", function(e){ - if($("#modals .modal:visible").length > 0){ - // Espace close modal - if(e.which === 27){ - closeModal(); - e.stopPropagation(); - } + // Escape close modal + if($(".modal:visible", $modals).length > 0 && e.which === 27){ + closeModal(); + e.stopPropagation(); } }); - function closeModal($modal){ - $modal = $modal || $("#modals .modal:visible"); - $modal.fadeOut(150); + function closeModal(){ + $(".modal:visible", $modals).fadeOut(150); $overlay.fadeOut(150); $("html").removeClass("dropdown-active"); } -})(jQuery); \ No newline at end of file + + + $(document).ready(function(){ + buildModals($(".modal")); + $("#content").on("DOMNodeInserted", ".modal", function(e){ + buildModals($(e.target)); + }); + }); +})(document, jQuery); \ No newline at end of file diff --git a/assets/js/select-autosubmit.js b/assets/js/select-autosubmit.js index 035f4e14f1..b758ad606e 100644 --- a/assets/js/select-autosubmit.js +++ b/assets/js/select-autosubmit.js @@ -4,10 +4,10 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; - $(".select-autosubmit").change(function() { + $("body").on("change", ".select-autosubmit", function() { $(this).parents("form:first").submit(); }); })(jQuery); \ No newline at end of file diff --git a/assets/js/spoiler.js b/assets/js/spoiler.js index 058cf32012..3406b6c36a 100644 --- a/assets/js/spoiler.js +++ b/assets/js/spoiler.js @@ -4,18 +4,27 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function(document, $, undefined){ "use strict"; - $(".spoiler").each(function(){ - $(this).before($("", { - "text": "Afficher/Masquer le contenu masqué", - "class": "spoiler-title ico-after view", - "href": "#", - "click": function(e){ - $(this).next(".spoiler").toggle(); - e.preventDefault(); - } - })); + function buildSpoilers($elem){ + $elem.each(function(){ + $(this).before($("", { + text: "Afficher/Masquer le contenu masqué", + class: "spoiler-title ico-after view", + href: "#", + click: function(e){ + $(this).next(".spoiler").toggle(); + e.preventDefault(); + } + })); + }); + } + + $(document).ready(function(){ + buildSpoilers($("#content .spoiler")); + $("#content").on("DOMNodeInserted", ".spoiler", function(e){ + buildSpoilers($(e.target)); + }); }); -})(jQuery); \ No newline at end of file +})(document, jQuery); \ No newline at end of file diff --git a/assets/js/tab-modalize.js b/assets/js/tab-modalize.js index 8b42664dde..7ed75907bf 100644 --- a/assets/js/tab-modalize.js +++ b/assets/js/tab-modalize.js @@ -4,7 +4,7 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; $("body").on("keydown", function(e){ diff --git a/assets/js/zen-mode.js b/assets/js/zen-mode.js index c83c564f21..50daef7f9c 100644 --- a/assets/js/zen-mode.js +++ b/assets/js/zen-mode.js @@ -4,14 +4,14 @@ Author: Alex-D / Alexandre Demode ========================================================================== */ -(function($){ +(function($, undefined){ "use strict"; if($(".article-content").length > 0){ $(".content-container .taglist ~ .authors").before($("
@@ -381,7 +393,6 @@ data-title="Mon compte" {% if not perms.forum.change_post %} - tabindex="-1" data-active="open-my-account" {% endif %} > @@ -421,7 +432,6 @@ Validation des articles {% endif %} -
  • {% csrf_token %} -
    -
  • + + +
    @@ -553,8 +564,7 @@

    {{ headlin {% else %} - - + {% endif %} @@ -567,7 +577,7 @@

    {{ headlin displayMath: [['$$','$$']], processEscapes: true, }, - TeX: { extensions: ["color.js", "cancel.js", "enclose.js", "bbox.js", "mathchoice.js", "newcommand.js", "verb.js", "unicode.js", "autobold.js"] }, + TeX: { extensions: ["color.js", "cancel.js", "enclose.js", "bbox.js", "mathchoice.js", "newcommand.js", "verb.js", "unicode.js", "autobold.js", "mhchem.js"] }, messageStyle: "none", }); diff --git a/templates/email/assoc/subscribe.html b/templates/email/assoc/subscribe.html index e214d40e25..be2a2d2f71 100644 --- a/templates/email/assoc/subscribe.html +++ b/templates/email/assoc/subscribe.html @@ -7,15 +7,13 @@

    - Le membre {{ username }} demande à adhérer à Zeste de Savoir. + Le membre {{ username }} souhaiterait adhérer à Zeste de Savoir.

      -
    • Prénom : {{ first_name }}
    • -
    • Nom de famille : {{ surname }}
    • +
    • Identité : {{ full_name }}
    • Adresse courriel : {{ email }}
    • -
    • Adresse : {{ adresse }}, {{ adresse_complement }}
    • -
    • CP et ville : {{ code_postal }} {{ ville }}
    • -
    • Pays : {{ pays }}
    • +
    • Date de naissance : {{ naissance }}
    • +
    • Adresse : {{ adresse|linebreaks }}

    Raison de cette demande : diff --git a/templates/email/assoc/subscribe.txt b/templates/email/assoc/subscribe.txt index 6feb56e9d2..b08e7f4296 100644 --- a/templates/email/assoc/subscribe.txt +++ b/templates/email/assoc/subscribe.txt @@ -2,16 +2,15 @@ Bonjour, membres du CA ! Le membre {{ username }} demande à adhérer à Zeste de Savoir ! -Prénom : {{ first_name }} -Nom de famille : {{ surname }} +Identité : {{ full_name }} Adresse courriel : {{ email }} -Adresse : {{ adresse }}, {{ adresse_complement }} -CP et ville : {{ code_postal }} {{ ville }} -Pays : {{ pays }} +Date de naissance : {{ naissance }} +Adresse : +{{ adresse }} Raison de cette demande : {{ justification }} -Cliquez ici pour voir son profil sur le site : {{ profile_url }} +Lien du profil sur le site : {{ profile_url }} Cordialement, Clem \ No newline at end of file diff --git a/templates/email/register/confirm.html b/templates/email/register/confirm.html index 957d5fab25..c14fbacb85 100644 --- a/templates/email/register/confirm.html +++ b/templates/email/register/confirm.html @@ -7,12 +7,18 @@

    - Merci de votre inscription sur Zeste de Savoir. Pour activer votre profil, cliquez sur le lien ci-dessous : + Vous vous êtes inscrit sur Zeste de Savoir et nous vous en remercions. + En étant membre de notre communauté, vous aurez la possibilité de rédiger des tutoriels et des articles + que vous pourrez ensuite publier et ainsi de rendre vos connaissances accessibles au plus grand nombre. + Vous pourrez également échanger avec les autres membres sur nos forums ! +
    + Il ne vous reste plus qu'à activer votre profil ! Pour ce faire, visitez le lien ci-dessous :
    {{ url }} +
    +
    + A très bientôt ! +
    + L'équipe Zeste de Savoir

    -
    - Cordialement, -
    - L'équipe Zeste de Savoir - \ No newline at end of file + diff --git a/templates/email/register/confirm.txt b/templates/email/register/confirm.txt index d5a52a1a1b..82a99fef3d 100644 --- a/templates/email/register/confirm.txt +++ b/templates/email/register/confirm.txt @@ -1,8 +1,10 @@ Bonjour {{ username }}, -Merci de votre inscription sur Zeste de Savoir. Pour activer votre profil, cliquez ou recopiez le lien ci-dessous : +Vous vous êtes inscrit sur Zeste de Savoir et nous vous en remercions. En étant membre de notre communauté, vous aurez la possibilité de rédiger des tutoriels et des articles que vous pourrez ensuite publier et ainsi rendre votre savoir accessible au plus grand nombre. Vous pourrez également échanger avec les autres membres sur nos forums ! + +Il ne vous reste plus qu'à activer votre profil ! Pour ce faire, visitez le lien ci-dessous : {{ url }} -Cordialement, -L'équipe Zeste de Savoir \ No newline at end of file +A très bientôt ! +L'équipe Zeste de Savoir diff --git a/templates/forum/base.html b/templates/forum/base.html index ba8a157e07..b7ba97f976 100644 --- a/templates/forum/base.html +++ b/templates/forum/base.html @@ -155,14 +155,6 @@

    {{ period|humane_delta }}

    {% endif %} - + {% block feeds_rss %} {% endblock %} {% endblock %} diff --git a/templates/forum/category/forum.html b/templates/forum/category/forum.html index cd337c146c..e321d16742 100644 --- a/templates/forum/category/forum.html +++ b/templates/forum/category/forum.html @@ -11,7 +11,9 @@ {% endif %} {% endblock %} - +{% block feeds_rss %} + {% include "forum/includes/feed.part.html" with param="?forum=" value=forum.pk %} +{% endblock %} {% block breadcrumb %}
  • diff --git a/templates/forum/category/index.html b/templates/forum/category/index.html index 75a5f3b414..fbec0f2eb8 100644 --- a/templates/forum/category/index.html +++ b/templates/forum/category/index.html @@ -24,4 +24,8 @@ +{% endblock %} + +{% block feeds_rss %} + {% include "forum/includes/feed.part.html" %} {% endblock %} \ No newline at end of file diff --git a/templates/forum/find/topic_by_tag.html b/templates/forum/find/topic_by_tag.html index e1c81d9579..eb28382014 100644 --- a/templates/forum/find/topic_by_tag.html +++ b/templates/forum/find/topic_by_tag.html @@ -20,7 +20,9 @@ Tag : {{ tag.title }} {% endblock %} - +{% block feeds_rss %} + {% include "forum/includes/feed.part.html" with param="?tag=" value=tag.pk %} +{% endblock %} {% block content %} {% include "misc/pagination.part.html" with position="top" %} diff --git a/templates/forum/includes/feed.part.html b/templates/forum/includes/feed.part.html new file mode 100644 index 0000000000..e65f2daf49 --- /dev/null +++ b/templates/forum/includes/feed.part.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/templates/forum/includes/forums.part.html b/templates/forum/includes/forums.part.html index 273c1dfbde..41a4013da5 100644 --- a/templates/forum/includes/forums.part.html +++ b/templates/forum/includes/forums.part.html @@ -1,5 +1,4 @@ {# Block displaying the list of forums belonging to a category #} -{% load forum %} @@ -36,4 +35,4 @@

    {% endwith %}

  • -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/templates/forum/index.html b/templates/forum/index.html index 42e3538b52..f8cb5ed989 100644 --- a/templates/forum/index.html +++ b/templates/forum/index.html @@ -37,7 +37,7 @@ {% endblock %} + +{% block feeds_rss %} + {% include "forum/includes/feed.part.html" %} +{% endblock %} \ No newline at end of file diff --git a/templates/forum/topic/index.html b/templates/forum/topic/index.html index d5df6cf1d5..3b8d81f010 100644 --- a/templates/forum/topic/index.html +++ b/templates/forum/topic/index.html @@ -11,8 +11,6 @@ {% if topic.is_solved %}[Résolu]{% endif %} {{ topic.title }} {% endblock %} - - {% block breadcrumb %}
  • + +{% endblock %} \ No newline at end of file diff --git a/templates/member/settings/user.html b/templates/member/settings/user.html index 36f5f76ff7..4c32c0d287 100644 --- a/templates/member/settings/user.html +++ b/templates/member/settings/user.html @@ -4,20 +4,19 @@ {% block title %} - Pseudo et e-mail + Pseudo et courriel {% endblock %} {% block breadcrumb %} -
  • Paramètres
  • -
  • Pseudo et e-mail
  • +
  • Pseudo et courriel
  • {% endblock %} {% block headline %} - Pseudo et e-mail + Pseudo et courriel {% endblock %} diff --git a/templates/misc/message.part.html b/templates/misc/message.part.html index 4bdfadd5c2..09445d86d6 100644 --- a/templates/misc/message.part.html +++ b/templates/misc/message.part.html @@ -46,7 +46,7 @@ {% if message.author = user or perms_change %} {% if can_hide != False %}
  • - + Masquer
  • - + Démasquer @@ -191,7 +191,7 @@
    {{ alert.pubdate|format_date|capfirst }} par {% include "misc/member_item.part.html" with member=alert.author %} : - {{ alert.text }} + {{ alert.text }} Résoudre diff --git a/templates/mp/post/new.html b/templates/mp/post/new.html index 5ebe91c149..19cf91e5e5 100644 --- a/templates/mp/post/new.html +++ b/templates/mp/post/new.html @@ -46,4 +46,20 @@ {% if form.text.value %} {% include "misc/previsualization.part.html" with text=form.text.value %} {% endif %} + +
    + +
    + {% for message in posts %} + {% captureas edit_link %} + {% url "zds.mp.views.edit_post" %}?message={{ message.pk }} + {% endcaptureas %} + + {% captureas cite_link %} + {% url "zds.mp.views.answer" %}?sujet={{ topic.pk }}&cite={{ message.pk }} + {% endcaptureas %} + + {% include "misc/message.part.html" %} + {% endfor %} +
    {% endblock %} diff --git a/templates/tutorial/base.html b/templates/tutorial/base.html index c3280a5f2e..25faa19ad7 100644 --- a/templates/tutorial/base.html +++ b/templates/tutorial/base.html @@ -92,7 +92,7 @@

    Télécharger

    {% endif %}
  • - + Archive
  • diff --git a/templates/tutorial/comment/new.html b/templates/tutorial/comment/new.html index 307d5dc2b7..47ccca35c1 100644 --- a/templates/tutorial/comment/new.html +++ b/templates/tutorial/comment/new.html @@ -1,6 +1,7 @@ {% extends "tutorial/base_online.html" %} {% load profile %} {% load crispy_forms_tags %} +{% load captureas %} @@ -42,4 +43,31 @@

    {{ tutorial.description }}

    {% if form.text.value %} {% include "misc/previsualization.part.html" with text=form.text.value %} {% endif %} + +
    + {% for message in notes %} + {% captureas edit_link %} + {% url "zds.tutorial.views.edit_note" %}?message={{ message.pk }} + {% endcaptureas %} + + {% captureas cite_link %} + {% url "zds.tutorial.views.answer" %}?tutorial={{ topic.pk }}&cite={{ message.pk }} + {% endcaptureas %} + + {% captureas upvote_link %} + {% url "zds.tutorial.views.like_note" %}?message={{ message.pk }} + {% endcaptureas %} + + {% captureas downvote_link %} + {% url "zds.tutorial.views.dislike_note" %}?message={{ message.pk }} + {% endcaptureas %} + + {% captureas alert_solve_link %} + {% url "zds.tutorial.views.solve_alert" %} + {% endcaptureas %} + + {% include "misc/message.part.html" with perms_change=perms.tutorial.change_tutorial %} + {% endfor %} +
    + {% endblock %} \ No newline at end of file diff --git a/templates/tutorial/index.html b/templates/tutorial/index.html index 07ecdfd8b6..975c9dae30 100644 --- a/templates/tutorial/index.html +++ b/templates/tutorial/index.html @@ -88,5 +88,20 @@ {% endfor %} {% endwith %} + {% endblock %} \ No newline at end of file diff --git a/templates/tutorial/tutorial/history.html b/templates/tutorial/tutorial/history.html index 09cfe41c57..36f4a80e93 100644 --- a/templates/tutorial/tutorial/history.html +++ b/templates/tutorial/tutorial/history.html @@ -1,7 +1,7 @@ {% extends "tutorial/base.html" %} -{% load emarkdown %} {% load profile %} {% load thumbnail %} +{% load date %} {% block title %} diff --git a/templates/tutorial/tutorial/import.html b/templates/tutorial/tutorial/import.html index ccf831b52f..46c606c516 100644 --- a/templates/tutorial/tutorial/import.html +++ b/templates/tutorial/tutorial/import.html @@ -24,12 +24,17 @@

    {% block content %} +

    Envoi d'une archive

    - Vous êtes l'auteur d'un cours sur le SdZ ? Nous l'avons récupéré pour vous ! Il est disponible hors-ligne et n'attend plus que votre accord pour être publié. Il vous suffit d'en faire la demande à un membre du Staff via le forum ou directement par MP. + Vous avez commencé a rédiger votre tutoriel sur l'éditeur en ligne de Zeste de Savoir, vous avez téléchargé l'archive correspondate et vous avez fait des modifications sur les fichiers en hors-ligne, et vous souhaitez maintenant importer ces modifications sur le site ? Faites une archive zip (seul format supporté actuellement) du répertoire dans lequel se trouve les fichiers de votre tutoriel et renseignez les deux champs ci-dessous, puis cliquez sur importer.

    - + {% crispy form_archive %}

    Envoi de fichiers .tuto

    +

    + Vous êtes l'auteur d'un cours sur le SdZ ? Nous l'avons récupéré pour vous ! Il est disponible en hors-ligne et n'attend plus que votre accord pour être publié. Il vous suffit d'en faire la demande à un membre du Staff via le forum ou directement par MP. +

    +

    Attention, le fichier attendu n'est pas un .tuto du Site du Zéro, mais un format intermédiaire qui contient du Markdown et non du zCode. Contactez un membre du Staff pour demander la conversion de votre fichier.

    diff --git a/templates/tutorial/tutorial/view_online.html b/templates/tutorial/tutorial/view_online.html index 4fdcb85d06..14a21ff470 100644 --- a/templates/tutorial/tutorial/view_online.html +++ b/templates/tutorial/tutorial/view_online.html @@ -51,7 +51,7 @@

    {% if tutorial.licence %} - Licence {{ tutorial.licence }} + {{ tutorial.licence }} {% endif %} diff --git a/zds/article/factories.py b/zds/article/factories.py index bc05157a9b..e2e1ec92c3 100644 --- a/zds/article/factories.py +++ b/zds/article/factories.py @@ -3,13 +3,6 @@ from datetime import datetime import factory from git.repo import Repo -try: - import ujson as json_reader -except: - try: - import simplejson as json_reader - except: - import json as json_reader import json as json_writer import os @@ -71,9 +64,10 @@ def _prepare(cls, create, **kwargs): class VaidationFactory(factory.DjangoModelFactory): FACTORY_FOR = Validation + class LicenceFactory(factory.DjangoModelFactory): FACTORY_FOR = Licence - + code = u'GNU_GPL' title = u'GNU General Public License' @@ -81,4 +75,3 @@ class LicenceFactory(factory.DjangoModelFactory): def _prepare(cls, create, **kwargs): licence = super(LicenceFactory, cls)._prepare(create, **kwargs) return licence - diff --git a/zds/article/forms.py b/zds/article/forms.py index 037c060872..051ccb5eef 100644 --- a/zds/article/forms.py +++ b/zds/article/forms.py @@ -52,7 +52,7 @@ class ArticleForm(forms.Form): queryset=SubCategory.objects.all(), required=False ) - + licence = forms.ModelChoiceField( label="Licence de votre publication", queryset=Licence.objects.all(), @@ -140,7 +140,7 @@ def __init__(self, article, user, *args, **kwargs): placeholder=u'Cet article est verrouillé.', disabled=True ) - + def clean(self): cleaned_data = super(ReactionForm, self).clean() diff --git a/zds/article/models.py b/zds/article/models.py index ff9dca5729..50b3a6da2c 100644 --- a/zds/article/models.py +++ b/zds/article/models.py @@ -1,14 +1,14 @@ # coding: utf-8 -from cStringIO import StringIO from django.conf import settings -from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models from math import ceil from git import Repo import os import string import uuid +import shutil + from easy_thumbnails.fields import ThumbnailerImageField try: @@ -68,7 +68,7 @@ class Meta: is_visible = models.BooleanField('Visible en rédaction', default=False, db_index=True) sha_public = models.CharField('Sha1 de la version publique', - blank=True, null=True, max_length=80,db_index=True) + blank=True, null=True, max_length=80, db_index=True) sha_validation = models.CharField('Sha1 de la version en validation', blank=True, null=True, max_length=80, db_index=True) sha_draft = models.CharField('Sha1 de la version de rédaction', @@ -92,6 +92,14 @@ class Meta: def __unicode__(self): return self.title + def delete_entity_and_tree(self): + """deletes the entity and its filesystem counterpart""" + shutil.rmtree(self.get_path(), 0) + Validation.objects.filter(article=self).delete() + if self.on_line(): + shutil.rmtree(self.get_prod_path()) + self.delete() + def get_absolute_url(self): return reverse('zds.article.views.view', kwargs={'article_pk': self.pk, @@ -110,7 +118,7 @@ def get_edit_url(self): '?article={0}'.format(self.pk) def on_line(self): - return self.sha_public is not None + return (self.sha_public is not None) and (self.sha_public.strip() != '') def in_validation(self): return self.sha_validation is not None @@ -156,13 +164,13 @@ def load_dic(self, article_version): article_version['sha_public'] = self.sha_public article_version['last_read_reaction'] = self.last_read_reaction article_version['get_reaction_count'] = self.get_reaction_count - article_version['get_absolute_url'] = reverse('zds.article.views.view', + article_version['get_absolute_url'] = reverse('zds.article.views.view', args=[self.pk, self.slug]) - article_version['get_absolute_url_online'] = reverse('zds.article.views.view_online', + article_version['get_absolute_url_online'] = reverse('zds.article.views.view_online', args=[self.pk, slugify(article_version['title'])]) - + return article_version - + def dump_json(self, path=None): if path is None: man_path = os.path.join(self.get_path(), 'manifest.json') @@ -185,10 +193,10 @@ def get_text(self): def save(self, *args, **kwargs): self.slug = slugify(self.title) - + if has_changed(self, 'image') and self.image: old = get_old_field_value(self, 'image', 'objects') - + if old is not None and len(old.name) > 0: root = settings.MEDIA_ROOT name = os.path.join(root, old.name) @@ -227,7 +235,7 @@ def last_read_reaction(self): .latest('reaction__pubdate').reaction except: return self.first_reaction() - + def first_unread_reaction(self): """Return the first reaction the user has unread.""" try: diff --git a/zds/article/tests.py b/zds/article/tests.py index 40abce441e..a56f80f6e4 100644 --- a/zds/article/tests.py +++ b/zds/article/tests.py @@ -1,6 +1,16 @@ # coding: utf-8 import os import shutil +import tempfile +import zipfile + +try: + import ujson as json_reader +except: + try: + import simplejson as json_reader + except: + import json as json_reader from django.conf import settings from django.core import mail @@ -34,7 +44,7 @@ def setUp(self): self.user_author = ProfileFactory().user self.user = ProfileFactory().user self.staff = StaffProfileFactory().user - + self.licence = LicenceFactory() self.article = ArticleFactory() @@ -397,14 +407,14 @@ def test_workflow_licence(self): # change licence (get 302) : result = self.client.post( - reverse('zds.article.views.edit') + - '?article={}'.format(self.article.pk), + reverse('zds.article.views.edit') + + '?article={}'.format(self.article.pk), { 'title': self.article.title, 'description': self.article.description, 'text': self.article.get_text(), 'subcategory': self.article.subcategory.all(), - 'licence' : new_licence.pk + 'licence': new_licence.pk }, follow=False) self.assertEqual(result.status_code, 302) @@ -429,14 +439,14 @@ def test_workflow_licence(self): # change licence back to old one (get 302, staff can change licence) : result = self.client.post( - reverse('zds.article.views.edit') + - '?article={}'.format(self.article.pk), + reverse('zds.article.views.edit') + + '?article={}'.format(self.article.pk), { 'title': self.article.title, 'description': self.article.description, 'text': self.article.get_text(), 'subcategory': self.article.subcategory.all(), - 'licence' : self.licence.pk + 'licence': self.licence.pk }, follow=False) self.assertEqual(result.status_code, 302) @@ -455,14 +465,14 @@ def test_workflow_licence(self): # change licence (get 302, redirection to login page) : result = self.client.post( - reverse('zds.article.views.edit') + - '?article={}'.format(self.article.pk), + reverse('zds.article.views.edit') + + '?article={}'.format(self.article.pk), { 'title': self.article.title, 'description': self.article.description, 'text': self.article.get_text(), 'subcategory': self.article.subcategory.all(), - 'licence' : new_licence.pk + 'licence': new_licence.pk }, follow=False) self.assertEqual(result.status_code, 302) @@ -471,7 +481,7 @@ def test_workflow_licence(self): article = Article.objects.get(pk=self.article.pk) self.assertEqual(article.licence.pk, self.licence.pk) self.assertNotEqual(article.licence.pk, new_licence.pk) - + # login with random user self.assertTrue( self.client.login( @@ -482,14 +492,14 @@ def test_workflow_licence(self): # change licence (get 403, random user cannot edit article if not in # authors list) : result = self.client.post( - reverse('zds.article.views.edit') + - '?article={}'.format(self.article.pk), + reverse('zds.article.views.edit') + + '?article={}'.format(self.article.pk), { 'title': self.article.title, 'description': self.article.description, 'text': self.article.get_text(), 'subcategory': self.article.subcategory.all(), - 'licence' : new_licence.pk + 'licence': new_licence.pk }, follow=False) self.assertEqual(result.status_code, 403) @@ -503,6 +513,144 @@ def test_workflow_licence(self): json = article.load_json() self.assertEquals(json['licence'], self.licence.code) + def test_workflow_archive_article(self): + """ensure the behavior of archive with an article""" + + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # modify article content and title (NOTE: zipfile does not ensure UTF-8) + article_title = u'Le titre, mais pas pareil' + article_content = u'Mais nous c\'est pas pareil ...' + result = self.client.post( + reverse('zds.article.views.edit') + + '?article={}'.format(self.article.pk), + { + 'title': article_title, + 'description': self.article.description, + 'text': article_content, + 'subcategory': self.article.subcategory.all(), + 'licence': self.licence.pk + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # now, draft and public version are not the same + article = Article.objects.get(pk=self.article.pk) + self.assertNotEqual(article.sha_draft, article.sha_public) + + # fetch archives : + # 1. draft version + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 200) + draft_zip_path = os.path.join(tempfile.gettempdir(), '__draft.zip') + f = open(draft_zip_path, 'w') + f.write(result.content) + f.close() + # 2. online version : + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}&online'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 200) + online_zip_path = os.path.join(tempfile.gettempdir(), '__online.zip') + f = open(online_zip_path, 'w') + f.write(result.content) + f.close() + + # now check if modification are in draft version of archive and not in the public one + draft_zip = zipfile.ZipFile(draft_zip_path, 'r') + online_zip = zipfile.ZipFile(online_zip_path, 'r') + + # first, test in manifest + online_manifest = json_reader.loads(online_zip.read('manifest.json')) + self.assertNotEqual(online_manifest['title'], article_title) # title has not changed in online version + + draft_manifest = json_reader.loads(draft_zip.read('manifest.json')) + self.assertNotEqual(online_manifest['title'], article_title) # title has not changed in online version + + self.assertNotEqual(online_zip.read(online_manifest['text']), article_content) + self.assertEqual(draft_zip.read(draft_manifest['text']), article_content) # content is good in draft + + draft_zip.close() + online_zip.close() + + # then logout and test access + self.client.logout() + + # public cannot access to draft version of article + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 403) + # ... but can access to online version + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}&online'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 200) + + # login with random user + self.assertEqual( + self.client.login( + username=self.user.username, + password='hostel77'), + True) + + # cannot access to draft version of article (if not author or staff) + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 403) + # but can access to online one + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}&online'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 200) + self.client.logout() + + # login with staff user + self.assertEqual( + self.client.login( + username=self.staff.username, + password='hostel77'), + True) + + # staff can access to draft version of article + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 200) + # ... and also to online version + result = self.client.get( + reverse('zds.article.views.download') + + '?article={0}&online'.format( + self.article.pk), + follow=False) + self.assertEqual(result.status_code, 200) + + # finally, clean up things: + os.remove(draft_zip_path) + os.remove(online_zip_path) + def tearDown(self): if os.path.isdir(settings.REPO_ARTICLE_PATH): shutil.rmtree(settings.REPO_ARTICLE_PATH) diff --git a/zds/article/views.py b/zds/article/views.py index 3c7d88a214..0aa1f900c6 100644 --- a/zds/article/views.py +++ b/zds/article/views.py @@ -13,6 +13,8 @@ import json as json_writer import os import shutil +import zipfile +import tempfile from django.conf import settings from django.contrib import messages @@ -27,13 +29,13 @@ from django.shortcuts import get_object_or_404, redirect from django.utils.encoding import smart_str from django.views.decorators.http import require_POST -from git import * +from git import Repo, Actor from zds.member.decorator import can_write_and_read_now from zds.member.views import get_client_ip from zds.utils import render_template from zds.utils import slugify -from zds.utils.articles import * +from zds.utils.articles import get_blob from zds.utils.mps import send_mp from zds.utils.models import SubCategory, Category, CommentLike, \ CommentDislike, Alert, Licence @@ -66,12 +68,12 @@ def index(request): .filter(sha_public__isnull=False, subcategory__in=[tag])\ .exclude(sha_public="").order_by('-pubdate')\ .all() - + article_versions = [] for article in articles: article_version = article.load_json_for_public() article_version = article.load_dic(article_version) - article_versions.append(article_version) + article_versions.append(article_version) return render_template('article/index.html', { 'articles': article_versions, @@ -111,8 +113,8 @@ def view(request, article_pk, article_slug): article_version = article.load_dic(article_version) validation = Validation.objects.filter(article__pk=article.pk)\ - .order_by("-date_proposition")\ - .first() + .order_by("-date_proposition")\ + .first() return render_template('article/member/view.html', { 'article': article_version, @@ -214,7 +216,7 @@ def new(request): 'description': request.POST['description'], 'text': request.POST['text'], 'image': image, - 'subcategory': request.POST.getlist('subcategory'), + 'subcategory': request.POST.getlist('subcategory'), 'licence': request.POST['licence'] }) return render_template('article/member/new.html', { @@ -269,8 +271,8 @@ def new(request): else: form = ArticleForm( initial={ - 'licence' : Licence.objects.get(pk=settings.DEFAULT_LICENCE_PK) - } + 'licence': Licence.objects.get(pk=settings.DEFAULT_LICENCE_PK) + } ) return render_template('article/member/new.html', { @@ -296,6 +298,28 @@ def edit(request): json = article.load_json() if request.method == 'POST': + # Using the "preview button" + if 'preview' in request.POST: + image = None + licence = None + if 'image' in request.FILES: + image = request.FILES['image'] + if 'licence' in request.POST: + licence = request.POST['licence'] + form = ArticleForm(initial={ + 'title': request.POST['title'], + 'description': request.POST['description'], + 'text': request.POST['text'], + 'image': image, + 'subcategory': request.POST.getlist('subcategory'), + 'licence': licence + }) + return render_template('article/member/edit.html', { + 'article': article, + 'text': request.POST['text'], + 'form': form + }) + form = ArticleForm(request.POST, request.FILES) if form.is_valid(): # Update article with data. @@ -318,7 +342,6 @@ def edit(request): article.licence = Licence.objects.get( pk=settings.DEFAULT_LICENCE_PK ) - article.save() @@ -339,14 +362,14 @@ def edit(request): licence = Licence.objects.filter(code=json["licence"]).all()[0] else: licence = Licence.objects.get( - pk=settings.DEFAULT_LICENCE_PK + pk=settings.DEFAULT_LICENCE_PK ) form = ArticleForm(initial={ 'title': json['title'], 'description': json['description'], 'text': article.get_text(), 'subcategory': article.subcategory.all(), - 'licence' : licence + 'licence': licence }) return render_template('article/member/edit.html', { @@ -361,12 +384,12 @@ def find_article(request, pk_user): .filter(authors__in=[user], sha_public__isnull=False).exclude(sha_public="")\ .order_by('-pubdate')\ .all() - + article_versions = [] for article in articles: article_version = article.load_json_for_public() article_version = article.load_dic(article_version) - article_versions.append(article_version) + article_versions.append(article_version) # Paginator return render_template('article/find.html', { @@ -419,25 +442,33 @@ def maj_repo_article( article.save() -def download(request): - """Download an article.""" - - article = get_object_or_404(Article, pk=request.GET['article']) - - ph = os.path.join(settings.REPO_ARTICLE_PATH, article.get_phy_slug()) - repo = Repo(ph) - repo.archive(open(ph + ".tar", 'w')) +def insert_into_zip(zip_file, git_tree): + """recursively add files from a git_tree to a zip archive""" + for blob in git_tree.blobs: # first, add files : + zip_file.writestr(blob.path, blob.data_stream.read()) + if len(git_tree.trees) is not 0: # then, recursively add dirs : + for subtree in git_tree.trees: + insert_into_zip(zip_file, subtree) - response = HttpResponse( - open( - ph + - ".tar", - 'rb').read(), - mimetype='application/tar') - response[ - 'Content-Disposition'] = 'attachment; filename={0}.tar' \ - .format(article.slug) +def download(request): + """Download an article.""" + article = get_object_or_404(Article, pk=request.GET["article"]) + repo_path = os.path.join(settings.REPO_ARTICLE_PATH, article.get_phy_slug()) + repo = Repo(repo_path) + sha = article.sha_draft + if 'online' in request.GET and article.on_line(): + sha = article.sha_public + elif request.user not in article.authors.all(): + if not request.user.has_perm('article.change_article'): + raise PermissionDenied # Only authors can download draft version + zip_path = os.path.join(tempfile.gettempdir(), article.slug + '.zip') + zip_file = zipfile.ZipFile(zip_path, 'w') + insert_into_zip(zip_file, repo.commit(sha).tree) + zip_file.close() + response = HttpResponse(open(zip_path, "rb").read(), content_type="application/zip") + response["Content-Disposition"] = "attachment; filename={0}.zip".format(article.slug) + os.remove(zip_path) return response # Validation @@ -479,19 +510,19 @@ def modify(request): # send feedback for author in article.authors.all(): msg = (u'Désolé **{0}**, ton zeste **{1}** ' - u'n\'a malheureusement pas passé l’étape de validation. ' - u'Mais ne désespère pas, certaines corrections peuvent ' - u'sûrement être faites pour l’améliorer et repasser la ' - u'validation plus tard. Voici le message que [{2}]({3}), ' - u'ton validateur t\'a laissé\n\n> {4}\n\nN\'hésite pas a ' - u'lui envoyer un petit message pour discuter de la décision ' - u'ou demander plus de détail si tout cela te semble ' - u'injuste ou manque de clarté.'.format( - author.username, - article.title, - validation.validator.username, - validation.validator.profile.get_absolute_url(), - validation.comment_validator)) + u'n\'a malheureusement pas passé l’étape de validation. ' + u'Mais ne désespère pas, certaines corrections peuvent ' + u'sûrement être faites pour l’améliorer et repasser la ' + u'validation plus tard. Voici le message que [{2}]({3}), ' + u'ton validateur t\'a laissé\n\n> {4}\n\nN\'hésite pas a ' + u'lui envoyer un petit message pour discuter de la décision ' + u'ou demander plus de détail si tout cela te semble ' + u'injuste ou manque de clarté.'.format( + author.username, + article.title, + validation.validator.username, + validation.validator.profile.get_absolute_url(), + validation.comment_validator)) bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) send_mp( bot, @@ -509,8 +540,8 @@ def modify(request): validation.version) else: messages.error(request, - "Vous devez avoir réservé cet article " - "pour pouvoir le refuser.") + "Vous devez avoir réservé cet article " + "pour pouvoir le refuser.") return redirect( article.get_absolute_url() + '?version=' + @@ -541,7 +572,7 @@ def modify(request): # A validatir would like to valid an article in validation. We # must update sha_public with the current sha of the validation. elif 'valid-article' in request.POST: - MEP(article, article.sha_validation) + mep(article, article.sha_validation) validation = Validation.objects\ .filter(article__pk=article.pk, version=article.sha_validation)\ @@ -591,8 +622,8 @@ def modify(request): validation.version) else: messages.error(request, - "Vous devez avoir réservé cet article " - "pour pouvoir le publier.") + "Vous devez avoir réservé cet article " + "pour pouvoir le publier.") return redirect( article.get_absolute_url() + '?version=' + @@ -611,10 +642,16 @@ def modify(request): # User would like to validate his article. So we must save the # current sha (version) of the article to his sha_validation. elif 'pending' in request.POST: + old_validation = Validation.objects.filter(article__pk=article_pk, + status__in=['PENDING_V']).first() + if old_validation is not None: + old_validator = old_validation.validator + else: + old_validator = None # Delete old pending validation Validation.objects.filter(article__pk=article_pk, - status__in=['PENDING','PENDING_V'])\ - .delete() + status__in=['PENDING', 'PENDING_V'])\ + .delete() # Create new validation validation = Validation() @@ -624,6 +661,25 @@ def modify(request): validation.comment_authors = request.POST['comment'] validation.version = request.POST['version'] + if old_validator is not None: + validation.validator = old_validator + validation.date_reserve + bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) + msg = \ + (u'Bonjour {0},\n\n' + u'L\'article *{1}* que tu as réservé a été mis à jour en zone de validation, ' + u'pour retrouver les modifications qui ont été faites, je t\'invite à' + u'consulter l\'historique des versions' + u'\n\nMerci'.format(old_validator.username, article.title)) + send_mp( + bot, + [old_validator], + u"Mise à jour d'article : {0}".format(article.title), + "En validation", + msg, + False, + ) + validation.save() validation.article.sha_validation = request.POST['version'] @@ -652,6 +708,30 @@ def modify(request): u'la rédaction de l\'article.'.format( author.username)) + # send msg to new author + + msg = ( + u'Bonjour **{0}**,\n\n' + u'Tu as été ajouté comme auteur de l\'article [{1}]({2}).\n' + u'Tu peux retrouver cet article en [cliquant ici]({3}), ou *via* le lien "En rédaction" du menu ' + u'"Articles" sur la page de ton profil.\n\n' + u'Tu peux maintenant commencer à rédiger !'.format( + author.username, + article.title, + settings.SITE_URL + article.get_absolute_url(), + settings.SITE_URL + reverse("zds.member.views.articles")) + ) + bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) + send_mp( + bot, + [author], + u"Ajout en tant qu'auteur : {0}".format(article.title), + "", + msg, + True, + direct=False, + ) + return redirect(redirect_url) elif 'remove_author' in request.POST: @@ -675,6 +755,27 @@ def modify(request): u'L\'auteur {0} a bien été retiré de l\'article.'.format( author.username)) + # send msg to removed author + + msg = ( + u'Bonjour **{0}**,\n\n' + u'Tu as été supprimé des auteurs de l\'article [{1}]({2}). Tant qu\'il ne sera pas publié, tu ne ' + u'pourra plus y accéder.\n'.format( + author.username, + article.title, + settings.SITE_URL + article.get_absolute_url()) + ) + bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) + send_mp( + bot, + [author], + u"Suppression des auteurs : {0}".format(article.title), + "", + msg, + True, + direct=False, + ) + return redirect(redirect_url) return redirect(article.get_absolute_url()) @@ -831,7 +932,7 @@ def history(request, article_pk, article_slug): # Reactions at an article. -def MEP(article, sha): +def mep(article, sha): # convert markdown file to html file repo = Repo(article.get_path()) manifest = get_blob(repo.commit(sha).tree, 'manifest.json') @@ -869,11 +970,6 @@ def answer(request): if article.antispam(request.user): raise PermissionDenied - # Retrieve 3 last reactions of the currenta article. - reactions = Reaction.objects\ - .filter(article=article)\ - .order_by('-pubdate')[:3] - # If there is a last reaction for the article, we save his pk. # Otherwise, we save 0. if article.last_reaction: @@ -881,6 +977,11 @@ def answer(request): else: last_reaction_pk = 0 + # Retrieve lasts reactions of the current topic. + reactions = Reaction.objects.filter(article=article) \ + .prefetch_related() \ + .order_by("-pubdate")[:settings.POSTS_PER_PAGE] + # User would like preview his post or post a new reaction on the article. if request.method == 'POST': data = request.POST @@ -895,6 +996,7 @@ def answer(request): 'article': article, 'last_reaction_pk': last_reaction_pk, 'newreaction': newreaction, + 'reactions': reactions, 'form': form }) @@ -923,6 +1025,7 @@ def answer(request): 'article': article, 'last_reaction_pk': last_reaction_pk, 'newreaction': newreaction, + 'reactions': reactions, 'form': form }) @@ -970,21 +1073,20 @@ def solve_alert(request): reaction = Reaction.objects.get(pk=alert.comment.id) bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) msg = (u'Bonjour {0},\n\nVous recevez ce message car vous avez ' - u'signalé le message de *{1}*, dans l\'article [{2}]({3}). ' - u'Votre alerte a été traitée par **{4}** et il vous a laissé ' - u'le message suivant :\n\n> {5}\n\nToute l\'équipe de ' - u'la modération vous remercie !'.format( - alert.author.username, - reaction.author.username, - reaction.article.title, - settings.SITE_URL + - reaction.get_absolute_url(), - request.user.username, - request.POST['text'])) + u'signalé le message de *{1}*, dans l\'article [{2}]({3}). ' + u'Votre alerte a été traitée par **{4}** et il vous a laissé ' + u'le message suivant :\n\n> {5}\n\nToute l\'équipe de ' + u'la modération vous remercie !'.format( + alert.author.username, + reaction.author.username, + reaction.article.title, + settings.SITE_URL + + reaction.get_absolute_url(), + request.user.username, + request.POST['text'])) send_mp( bot, [ - alert.author], u"Résolution d'alerte : {0}".format( - reaction.article.title), "", msg, False) + alert.author], u"Résolution d'alerte : {0}".format(reaction.article.title), "", msg, False) alert.delete() messages.success( diff --git a/zds/forum/factories.py b/zds/forum/factories.py index f56e0a2c1f..7ec2f295af 100644 --- a/zds/forum/factories.py +++ b/zds/forum/factories.py @@ -4,6 +4,7 @@ from zds.forum.models import Category, Forum, Topic, Post from zds.utils.models import Tag + class CategoryFactory(factory.DjangoModelFactory): FACTORY_FOR = Category @@ -19,12 +20,14 @@ class ForumFactory(factory.DjangoModelFactory): lambda n: 'Sous Titre du Forum No{0}'.format(n)) slug = factory.Sequence(lambda n: 'forum{0}'.format(n)) + class TagFactory(factory.DjangoModelFactory): FACTORY_FOR = Tag - + title = factory.Sequence(lambda n: 'Tag{0}'.format(n)) slug = factory.Sequence(lambda n: 'tag{0}'.format(n)) + class TopicFactory(factory.DjangoModelFactory): FACTORY_FOR = Topic diff --git a/zds/forum/feeds.py b/zds/forum/feeds.py index e3fda2ddeb..013d655e15 100644 --- a/zds/forum/feeds.py +++ b/zds/forum/feeds.py @@ -3,6 +3,7 @@ from django.contrib.syndication.views import Feed from django.utils.feedgenerator import Atom1Feed +from django.conf import settings from zds.utils.templatetags.emarkdown import emarkdown @@ -13,12 +14,35 @@ class LastPostsFeedRSS(Feed): title = u'Derniers messages sur Zeste de Savoir' link = '/forums/' description = (u'Les derniers messages ' - u'parus sur le forum de Zeste de Savoir.') - - def items(self): - posts = Post.objects.filter(topic__forum__group__isnull=True)\ - .order_by('-pubdate') - return posts[:5] + u'parus sur le forum de Zeste de Savoir.') + + def get_object(self, request): + obj = {} + if "forum" in request.GET: + obj['forum'] = request.GET["forum"] + if "tag" in request.GET: + obj['tag'] = request.GET["tag"] + return obj + + def items(self, obj): + if "forum" in obj and "tag" in obj: + posts = Post.objects.filter(topic__forum__group__isnull=True, + topic__forum__pk=obj['forum'], + topic__tags__pk__in=[obj['tag']])\ + .order_by('-pubdate') + elif "forum" in obj and "tag" not in obj: + posts = Post.objects.filter(topic__forum__group__isnull=True, + topic__forum__pk=obj['forum'])\ + .order_by('-pubdate') + elif "forum" not in obj and "tag" in obj: + posts = Post.objects.filter(topic__forum__group__isnull=True, + topic__tags__pk__in=[obj['tag']])\ + .order_by('-pubdate') + if "forum" not in obj and "tag" not in obj: + posts = Post.objects.filter(topic__forum__group__isnull=True)\ + .order_by('-pubdate') + + return posts[:settings.POSTS_PER_PAGE] def item_title(self, item): return u'{}, message #{}'.format(item.topic.title, item.pk) @@ -50,10 +74,34 @@ class LastTopicsFeedRSS(Feed): link = '/forums/' description = u'Les derniers sujets créés sur le forum de Zeste de Savoir.' - def items(self): - topics = Topic.objects.filter(forum__group__isnull=True)\ - .order_by('-pubdate') - return topics[:5] + def get_object(self, request): + obj = {} + if "forum" in request.GET: + obj['forum'] = request.GET["forum"] + if "tag" in request.GET: + obj['tag'] = request.GET["tag"] + return obj + + def items(self, obj): + if "forum" in obj and "tag" in obj: + topics = Topic.objects.filter(forum__group__isnull=True, + forum__pk=obj['forum'], + tags__pk__in=[obj['tag']])\ + .order_by('-pubdate') + elif "forum" in obj and "tag" not in obj: + topics = Topic.objects.filter(forum__group__isnull=True, + forum__pk=obj['forum'])\ + .order_by('-pubdate') + elif "forum" not in obj and "tag" in obj: + topics = Topic.objects.filter(forum__group__isnull=True, + tags__pk__in=[obj['tag']])\ + .order_by('-pubdate') + if "forum" not in obj and "tag" not in obj: + topics = Topic.objects.filter(forum__group__isnull=True)\ + .order_by('-pubdate') + + return topics[:settings.POSTS_PER_PAGE] + def item_pubdate(self, item): return item.pubdate diff --git a/zds/forum/migrations/0006_auto__add_field_topic_key.py b/zds/forum/migrations/0006_auto__add_field_topic_key.py new file mode 100644 index 0000000000..538c5bf883 --- /dev/null +++ b/zds/forum/migrations/0006_auto__add_field_topic_key.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Topic.key' + db.add_column(u'forum_topic', 'key', + self.gf('django.db.models.fields.IntegerField')(null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Topic.key' + db.delete_column(u'forum_topic', 'key') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'forum.category': { + 'Meta': {'object_name': 'Category'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'position': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'forum.forum': { + 'Meta': {'object_name': 'Forum'}, + 'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forum.Category']"}), + 'group': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['auth.Group']", 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'position_in_category': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'forum.post': { + 'Meta': {'object_name': 'Post', '_ormbases': [u'utils.Comment']}, + u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), + 'is_useful': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forum.Topic']"}) + }, + u'forum.topic': { + 'Meta': {'object_name': 'Topic'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics'", 'to': u"orm['auth.User']"}), + 'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forum.Forum']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_solved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'is_sticky': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'key': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'last_message': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_message'", 'null': 'True', 'to': u"orm['forum.Post']"}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.Tag']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'forum.topicfollowed': { + 'Meta': {'object_name': 'TopicFollowed'}, + 'email': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forum.Topic']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics_followed'", 'to': u"orm['auth.User']"}) + }, + u'forum.topicread': { + 'Meta': {'object_name': 'TopicRead'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forum.Post']"}), + 'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forum.Topic']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics_read'", 'to': u"orm['auth.User']"}) + }, + u'utils.comment': { + 'Meta': {'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), + 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), + 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), + 'text_html': ('django.db.models.fields.TextField', [], {}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'utils.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '20'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + } + } + + complete_apps = ['forum'] \ No newline at end of file diff --git a/zds/forum/models.py b/zds/forum/models.py index c8411fd7f9..0ca7e047b1 100644 --- a/zds/forum/models.py +++ b/zds/forum/models.py @@ -129,6 +129,7 @@ def is_read(self): return False return True + class Topic(models.Model): """A thread, containing posts.""" @@ -158,6 +159,8 @@ class Meta: blank=True, db_index=True) + key = models.IntegerField('cle', null=True, blank=True) + def __unicode__(self): """Textual form of a thread.""" return self.title @@ -193,7 +196,7 @@ def first_post(self): .order_by('pubdate')\ .first() - def add_tags(self,tag_collection): + def add_tags(self, tag_collection): for tag in tag_collection: tag_title = smart_text(tag.strip().lower()) current_tag = Tag.objects.filter(title=tag_title).first() @@ -245,7 +248,6 @@ def is_followed(self, user=None): return TopicFollowed.objects.filter(topic=self, user=user).exists() - def is_email_followed(self, user=None): """Check if the topic is currently email followed by the user. @@ -380,7 +382,7 @@ def follow(topic, user=None): """Toggle following of a topic for an user.""" ret = None if user is None: - user=get_current_user() + user = get_current_user() try: existing = TopicFollowed.objects.get( topic=topic, user=user @@ -402,14 +404,15 @@ def follow(topic, user=None): ret = False return ret + def follow_by_email(topic, user=None): """Toggle following of a topic for an user.""" ret = None if user is None: - user=get_current_user() + user = get_current_user() try: existing = TopicFollowed.objects.get( - topic=topic, \ + topic=topic, user=user ) except TopicFollowed.DoesNotExist: @@ -420,7 +423,7 @@ def follow_by_email(topic, user=None): t = TopicFollowed( topic=topic, user=user, - email = True + email=True ) t.save() ret = True @@ -445,21 +448,22 @@ def get_last_topics(user): break return tops + def get_topics(forum_pk, is_sticky, is_solved=None): """ Get topics according to parameters """ if is_solved is not None: return Topic.objects.filter( - forum__pk=forum_pk, - is_sticky=is_sticky, - is_solved=is_solved).order_by("-last_message__pubdate").prefetch_related( - "author", - "last_message", - "tags").all() + forum__pk=forum_pk, + is_sticky=is_sticky, + is_solved=is_solved).order_by("-last_message__pubdate").prefetch_related( + "author", + "last_message", + "tags").all() else: return Topic.objects.filter( - forum__pk=forum_pk, - is_sticky=is_sticky).order_by("-last_message__pubdate").prefetch_related( - "author", - "last_message", - "tags").all() + forum__pk=forum_pk, + is_sticky=is_sticky).order_by("-last_message__pubdate").prefetch_related( + "author", + "last_message", + "tags").all() diff --git a/zds/forum/tests.py b/zds/forum/tests.py index 886b14c743..57ec8231bf 100644 --- a/zds/forum/tests.py +++ b/zds/forum/tests.py @@ -9,12 +9,13 @@ from zds.forum.factories import CategoryFactory, ForumFactory, \ TopicFactory, PostFactory, TagFactory from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.utils.models import CommentLike, CommentDislike, Alert +from zds.utils.models import CommentLike, CommentDislike, Alert, Tag from django.core import mail from .models import Post, Topic, TopicFollowed, TopicRead from zds.forum.views import get_tag_by_title -from zds.forum.models import get_topics +from zds.forum.models import get_topics, Forum + class ForumMemberTests(TestCase): @@ -48,6 +49,29 @@ def setUp(self): password='hostel77') self.assertEqual(log, True) + def feed_rss_display(self): + """Test each rss feed feed""" + response = self.client.get(reverse('post-feed-rss'), follow=False) + self.assertEqual(response.status_code, 200) + + for forum in Forum.objects.all(): + response = self.client.get(reverse('post-feed-rss') + "?forum={}".format(forum.pk), follow=False) + self.assertEqual(response.status_code, 200) + + for tag in Tag.objects.all(): + response = self.client.get(reverse('post-feed-rss') + "?tag={}".format(tag.pk), follow=False) + self.assertEqual(response.status_code, 200) + + for forum in Forum.objects.all(): + for tag in Tag.objects.all(): + response = self.client.get( + reverse('post-feed-rss') + + "?tag={}&forum={}".format( + tag.pk, + forum.pk), + follow=False) + self.assertEqual(response.status_code, 200) + def test_display(self): """Test forum display (full: root, category, forum) Topic display test is in creation topic test.""" @@ -114,8 +138,6 @@ def test_create_topic(self): self.assertContains(response, topic.title) self.assertContains(response, topic.subtitle) - - def test_answer(self): """To test all aspects of answer.""" user1 = ProfileFactory().user @@ -176,7 +198,7 @@ def test_answer(self): Post.objects.get( pk=4).text, u'C\'est tout simplement l\'histoire de la ville de Paris que je voudrais vous conter ') - + # test antispam return 403 result = self.client.post( reverse('zds.forum.views.answer') + '?sujet={0}'.format(topic1.pk), @@ -186,7 +208,6 @@ def test_answer(self): }, follow=False) self.assertEqual(result.status_code, 403) - def test_edit_main_post(self): """To test all aspects of the edition of main post by member.""" @@ -562,14 +583,13 @@ def test_answer_empty(self): self.assertEqual(Post.objects.filter(topic=topic1.pk).count(), 1) def test_add_tag(self): - - + TagCSharp = TagFactory(title="C#") - + TagC = TagFactory(title="C") self.assertEqual(TagCSharp.slug, TagC.slug) self.assertNotEqual(TagCSharp.title, TagC.title) - #post a topic with a tag + # post a topic with a tag result = self.client.post( reverse('zds.forum.views.new') + '?forum={0}' .format(self.forum12.pk), @@ -579,16 +599,16 @@ def test_add_tag(self): }, follow=False) self.assertEqual(result.status_code, 302) - - #test the topic is added to the good tag - - self.assertEqual( Topic.objects.filter( - tags__in=[TagCSharp]) - .order_by("-last_message__pubdate").prefetch_related( - "tags").count(), 1) - self.assertEqual( Topic.objects.filter(tags__in=[TagC]) - .order_by("-last_message__pubdate").prefetch_related( - "tags").count(), 0) + + # test the topic is added to the good tag + + self.assertEqual(Topic.objects.filter( + tags__in=[TagCSharp]) + .order_by("-last_message__pubdate").prefetch_related( + "tags").count(), 1) + self.assertEqual(Topic.objects.filter(tags__in=[TagC]) + .order_by("-last_message__pubdate").prefetch_related( + "tags").count(), 0) topicWithConflictTags = TopicFactory( forum=self.forum11, author=self.user) topicWithConflictTags.title = u"[C][c][ c][C ]name" @@ -675,6 +695,29 @@ def setUp(self): position_in_category=2) self.user = ProfileFactory().user + def feed_rss_display(self): + """Test each rss feed feed""" + response = self.client.get(reverse('post-feed-rss'), follow=False) + self.assertEqual(response.status_code, 200) + + for forum in Forum.objects.all(): + response = self.client.get(reverse('post-feed-rss') + "?forum={}".format(forum.pk), follow=False) + self.assertEqual(response.status_code, 200) + + for tag in Tag.objects.all(): + response = self.client.get(reverse('post-feed-rss') + "?tag={}".format(tag.pk), follow=False) + self.assertEqual(response.status_code, 200) + + for forum in Forum.objects.all(): + for tag in Tag.objects.all(): + response = self.client.get( + reverse('post-feed-rss') + + "?tag={}&forum={}".format( + tag.pk, + forum.pk), + follow=False) + self.assertEqual(response.status_code, 200) + def test_display(self): """Test forum display (full: root, category, forum) Topic display test is in creation topic test.""" @@ -742,35 +785,33 @@ def test_answer(self): def test_tag_parsing(self): """test the tag parsing in nominal, limit and borns cases""" - (tags, title) = get_tag_by_title("[tag]title"); - self.assertEqual(len(tags),1) - self.assertEqual(title,"title") - - (tags,title) = get_tag_by_title("[[tag1][tag2]]title") - self.assertEqual(len(tags),1) - self.assertEqual(tags[0],"[tag1][tag2]") - self.assertEqual(title,"title") - - (tags,title) = get_tag_by_title("[tag1][tag2]title") - self.assertEqual(len(tags),2) - self.assertEqual(tags[0],"tag1") - self.assertEqual(title,"title") - (tags,title) = get_tag_by_title("[tag1] [tag2]title") - self.assertEqual(len(tags),2) - self.assertEqual(tags[0],"tag1") - self.assertEqual(title,"title") - - - (tags,title) = get_tag_by_title("[tag1][tag2]title[tag3]") - self.assertEqual(len(tags),2) - self.assertEqual(tags[0],"tag1") - self.assertEqual(title,"title[tag3]") - - (tags,title) = get_tag_by_title("[tag1[][tag2]title") - self.assertEqual(len(tags),0) - self.assertEqual(title,"[tag1[][tag2]title") - - + (tags, title) = get_tag_by_title("[tag]title") + self.assertEqual(len(tags), 1) + self.assertEqual(title, "title") + + (tags, title) = get_tag_by_title("[[tag1][tag2]]title") + self.assertEqual(len(tags), 1) + self.assertEqual(tags[0], "[tag1][tag2]") + self.assertEqual(title, "title") + + (tags, title) = get_tag_by_title("[tag1][tag2]title") + self.assertEqual(len(tags), 2) + self.assertEqual(tags[0], "tag1") + self.assertEqual(title, "title") + (tags, title) = get_tag_by_title("[tag1] [tag2]title") + self.assertEqual(len(tags), 2) + self.assertEqual(tags[0], "tag1") + self.assertEqual(title, "title") + + (tags, title) = get_tag_by_title("[tag1][tag2]title[tag3]") + self.assertEqual(len(tags), 2) + self.assertEqual(tags[0], "tag1") + self.assertEqual(title, "title[tag3]") + + (tags, title) = get_tag_by_title("[tag1[][tag2]title") + self.assertEqual(len(tags), 0) + self.assertEqual(title, "[tag1[][tag2]title") + def test_edit_main_post(self): """To test all aspects of the edition of main post by guest.""" topic1 = TopicFactory(forum=self.forum11, author=self.user) @@ -975,7 +1016,7 @@ def test_url_topic(self): def test_filter_topic(self): """Test filters for solved topic or not""" - user1 = ProfileFactory().user + ProfileFactory().user topic = TopicFactory(forum=self.forum11, author=self.user, is_solved=False, is_sticky=False) topic_solved = TopicFactory(forum=self.forum11, author=self.user, is_solved=True, is_sticky=False) topic_sticky = TopicFactory(forum=self.forum11, author=self.user, is_solved=False, is_sticky=True) @@ -992,7 +1033,6 @@ def test_filter_topic(self): self.assertEqual(len(get_topics(forum_pk=self.forum11.pk, is_sticky=True, is_solved=True)), 1) self.assertEqual(get_topics(forum_pk=self.forum11.pk, is_sticky=True, is_solved=True)[0], topic_solved_sticky) - + self.assertEqual(len(get_topics(forum_pk=self.forum11.pk, is_sticky=False)), 2) self.assertEqual(len(get_topics(forum_pk=self.forum11.pk, is_sticky=True)), 2) - \ No newline at end of file diff --git a/zds/forum/urls.py b/zds/forum/urls.py index 909f1467f8..73a42e3453 100644 --- a/zds/forum/urls.py +++ b/zds/forum/urls.py @@ -9,13 +9,9 @@ urlpatterns = patterns('', # Feeds - url(r'^flux/messages/rss/$', - feeds.LastPostsFeedRSS(), - name='post-feed-rss'), - url(r'^flux/messages/atom/$', - feeds.LastPostsFeedATOM(), - name='post-feed-atom'), - + url(r'^flux/messages/rss/$', feeds.LastPostsFeedRSS(), name='post-feed-rss'), + url(r'^flux/messages/atom/$', feeds.LastPostsFeedATOM(), name='post-feed-atom'), + url(r'^flux/sujets/rss/$', feeds.LastTopicsFeedRSS(), name='topic-feed-rss'), diff --git a/zds/forum/views.py b/zds/forum/views.py index 4615d1eadf..860b704484 100644 --- a/zds/forum/views.py +++ b/zds/forum/views.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- from datetime import datetime import json -import re from django.conf import settings from django.db.models import Q @@ -19,14 +18,13 @@ from django.template import Context from django.template.loader import get_template from django.views.decorators.http import require_POST -from django.utils.encoding import smart_text from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from forms import TopicForm, PostForm, MoveTopicForm from models import Category, Forum, Topic, Post, follow, follow_by_email, never_read, \ - mark_read, TopicFollowed, sub_tag, get_topics + mark_read, TopicFollowed, get_topics from zds.forum.models import TopicRead from zds.member.decorator import can_write_and_read_now from zds.member.views import get_client_ip @@ -47,7 +45,6 @@ def index(request): "user": request.user}) - def details(request, cat_slug, forum_slug): """Display the given forum and all its topics.""" @@ -91,31 +88,29 @@ def details(request, cat_slug, forum_slug): }) - def cat_details(request, cat_slug): """Display the forums belonging to the given category.""" - + category = get_object_or_404(Category, slug=cat_slug) - + forums_pub = Forum.objects\ - .filter(group__isnull=True, category__pk=category.pk)\ - .select_related("category").all() + .filter(group__isnull=True, category__pk=category.pk)\ + .select_related("category").all() if request.user.is_authenticated(): forums_prv = Forum.objects\ - .filter(group__isnull=False, \ - group__in=request.user.groups.all(), \ - category__pk=category.pk)\ - .select_related("category")\ - .all() - forums = forums_pub|forums_prv - else : + .filter(group__isnull=False, + group__in=request.user.groups.all(), + category__pk=category.pk)\ + .select_related("category")\ + .all() + forums = forums_pub | forums_prv + else: forums = forums_pub return render_template("forum/category/index.html", {"category": category, "forums": forums}) - def topic(request, topic_pk, topic_slug): """Display a thread and its posts using a pager.""" @@ -203,12 +198,12 @@ def get_tag_by_title(title): continue_parsing_tags = True original_title = title for char in title: - + if char == u"[" and nb_bracket == 0 and continue_parsing_tags: nb_bracket += 1 elif nb_bracket > 0 and char != u"]" and continue_parsing_tags: current_tag = current_tag + char - if char == u"[" : + if char == u"[": nb_bracket += 1 elif char == u"]" and nb_bracket > 0 and continue_parsing_tags: nb_bracket -= 1 @@ -217,14 +212,14 @@ def get_tag_by_title(title): current_tag = u"" elif current_tag.strip() != u"" and nb_bracket > 0: current_tag = current_tag + char - - elif ((char != u"[" and char.strip()!="") or not continue_parsing_tags): + + elif ((char != u"[" and char.strip() != "") or not continue_parsing_tags): continue_parsing_tags = False current_title = current_title + char title = current_title - #if we did not succed in parsing the tags - if nb_bracket != 0 : - return ([],original_title) + # if we did not succed in parsing the tags + if nb_bracket != 0: + return ([], original_title) return (tags, title.strip()) @@ -280,8 +275,7 @@ def new(request): post = Post() post.topic = n_topic post.author = request.user - post.text = data["text"] - post.text_html = emarkdown(request.POST["text"]) + post.update_content(request.POST["text"]) post.pubdate = datetime.now() post.position = 1 post.ip_address = get_client_ip(request) @@ -314,16 +308,16 @@ def solve_alert(request): bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) msg = \ (u'Bonjour {0},' - u'Vous recevez ce message car vous avez signalé le message de *{1}*, ' - u'dans le sujet [{2}]({3}). Votre alerte a été traitée par **{4}** ' - u'et il vous a laissé le message suivant :' - u'\n\n> {5}\n\nToute l\'équipe de la modération vous remercie !'.format( - alert.author.username, - post.author.username, - post.topic.title, - settings.SITE_URL + post.get_absolute_url(), - request.user.username, - request.POST["text"],)) + u'Vous recevez ce message car vous avez signalé le message de *{1}*, ' + u'dans le sujet [{2}]({3}). Votre alerte a été traitée par **{4}** ' + u'et il vous a laissé le message suivant :' + u'\n\n> {5}\n\nToute l\'équipe de la modération vous remercie !'.format( + alert.author.username, + post.author.username, + post.topic.title, + settings.SITE_URL + post.get_absolute_url(), + request.user.username, + request.POST["text"],)) send_mp( bot, [alert.author], @@ -387,7 +381,7 @@ def edit(request): try: page = int(request.POST["page"]) except: - #problem in variable format + # problem in variable format raise Http404 else: page = 1 @@ -466,13 +460,10 @@ def answer(request): raise PermissionDenied last_post_pk = g_topic.last_message.pk - # Retrieve 10 last posts of the current topic. - - posts = \ - Post.objects.filter(topic=g_topic) \ + # Retrieve last posts of the current topic. + posts = Post.objects.filter(topic=g_topic) \ .prefetch_related() \ - .order_by("-pubdate" - )[:10] + .order_by("-pubdate")[:settings.POSTS_PER_PAGE] # User would like preview his post or post a new post on the topic. @@ -483,8 +474,7 @@ def answer(request): # Using the « preview button », the « more » button or new post if "preview" in data or newpost: - form = PostForm(g_topic, request.user, initial={"text": data["text" - ]}) + form = PostForm(g_topic, request.user, initial={"text": data["text"]}) form.helper.form_action = reverse("zds.forum.views.answer") \ + "?sujet=" + str(g_topic.pk) return render_template("forum/post/new.html", { @@ -513,7 +503,7 @@ def answer(request): post.save() g_topic.last_message = post g_topic.save() - #Send mail + # Send mail subject = "ZDS - Notification : " + g_topic.title from_email = "Zeste de Savoir <{0}>".format(settings.MAIL_NOREPLY) followers = g_topic.get_followers_by_email() @@ -531,19 +521,17 @@ def answer(request): .render( Context({ 'username': receiver.username, - 'title':g_topic.title, + 'title': g_topic.title, 'url': settings.SITE_URL + post.get_absolute_url(), 'author': request.user.username - }) - ) + })) message_txt = get_template('email/notification/new.txt').render( Context({ 'username': receiver.username, - 'title':g_topic.title, + 'title': g_topic.title, 'url': settings.SITE_URL + post.get_absolute_url(), 'author': request.user.username - }) - ) + })) msg = EmailMultiAlternatives( subject, message_txt, from_email, [ receiver.email]) @@ -879,7 +867,6 @@ def dislike_post(request): return redirect(post.get_absolute_url()) - def find_topic_by_tag(request, tag_pk, tag_slug): """Finds all topics byg tag.""" @@ -893,18 +880,19 @@ def find_topic_by_tag(request, tag_pk, tag_slug): topics = Topic.objects.filter( tags__in=[tag], is_solved=True).order_by("-last_message__pubdate").prefetch_related( - "author", - "last_message", - "tags")\ + "author", + "last_message", + "tags")\ .exclude(Q(forum__group__isnull=False) & ~Q(forum__group__in=u.groups.all()))\ .all() else: topics = Topic.objects.filter( tags__in=[tag], - is_solved=False).order_by("-last_message__pubdate").prefetch_related( - "author", - "last_message", - "tags")\ + is_solved=False).order_by("-last_message__pubdate")\ + .prefetch_related( + "author", + "last_message", + "tags")\ .exclude(Q(forum__group__isnull=False) & ~Q(forum__group__in=u.groups.all()))\ .all() else: @@ -934,7 +922,6 @@ def find_topic_by_tag(request, tag_pk, tag_slug): }) - def find_topic(request, user_pk): """Finds all topics of a user.""" @@ -972,7 +959,7 @@ def find_post(request, user_pk): displayed_user = get_object_or_404(User, pk=user_pk) user = request.user - + if user.has_perm("forum.change_post"): posts = \ Post.objects.filter(author=displayed_user)\ diff --git a/zds/gallery/forms.py b/zds/gallery/forms.py index 7969ea1bb5..0694d2fe41 100644 --- a/zds/gallery/forms.py +++ b/zds/gallery/forms.py @@ -10,7 +10,6 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse -from zds.utils.forms import CommonLayoutModalText from zds.gallery.models import Gallery, Image @@ -104,6 +103,7 @@ def clean(self): return cleaned_data + class ImageForm(forms.Form): title = forms.CharField( label='Titre', @@ -135,8 +135,6 @@ def __init__(self, *args, **kwargs): Field('physical'), ButtonHolder( StrictButton('Ajouter', type='submit'), - HTML('Annuler'), ), ) @@ -146,12 +144,50 @@ def clean(self): physical = cleaned_data.get('physical') if physical is not None and physical.size > settings.IMAGE_MAX_SIZE: - self._errors['physical'] = self.error_class([u'Votre image est trop lourde, la limite autorisée est de : {0} Ko' - .format(settings.IMAGE_MAX_SIZE / 1024) + ' Ko']) + self._errors['physical'] = self.error_class( + [u'Votre image est trop lourde, la limite autorisée ' + u'est de : {0} Ko' .format(settings.IMAGE_MAX_SIZE / 1024) + ' Ko']) + return cleaned_data + + +class ArchiveImageForm(forms.Form): + file = forms.FileField( + label='Sélectionnez l\'archive contenant les images à charger', + required=True + ) + + def __init__(self, *args, **kwargs): + super(ArchiveImageForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'clearfix' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('file'), + ButtonHolder( + StrictButton('Importer', type='submit'), + HTML('Annuler'), + ), + ) + + def clean(self): + cleaned_data = super(ArchiveImageForm, self).clean() + + file = cleaned_data.get('file') + extension = file.name.split('.')[-1] + + if extension != "zip": + self._errors['file'] = self.error_class( + [u"Le champ n'accepte que les fichiers zip"]) + if 'file' in cleaned_data: + del cleaned_data['file'] + return cleaned_data class UpdateImageForm(ImageForm): + def __init__(self, *args, **kwargs): super(ImageForm, self).__init__(*args, **kwargs) @@ -167,13 +203,14 @@ def __init__(self, *args, **kwargs): Field('physical'), ButtonHolder( StrictButton(u'Mettre à jour', type='submit'), - HTML('Annuler'), ), ) + class ImageAsAvatarForm(forms.Form): + """"Form to add current image as avatar""" + def __init__(self, *args, **kwargs): super(ImageAsAvatarForm, self).__init__(*args, **kwargs) self.helper = FormHelper() diff --git a/zds/gallery/models.py b/zds/gallery/models.py index 9f449993c7..061181375d 100644 --- a/zds/gallery/models.py +++ b/zds/gallery/models.py @@ -10,6 +10,7 @@ from django.db import models from django.dispatch import receiver from easy_thumbnails.fields import ThumbnailerImageField +from zds.settings import MEDIA_ROOT def image_path(instance, filename): @@ -18,6 +19,7 @@ def image_path(instance, filename): filename = u'{}.{}'.format(str(uuid.uuid4()), string.lower(ext)) return os.path.join('galleries', str(instance.gallery.pk), filename) + class UserGallery(models.Model): class Meta: @@ -78,6 +80,7 @@ def get_absolute_url(self): def get_extension(self): return os.path.splitext(self.physical.name)[1][1:] + @receiver(models.signals.post_delete, sender=Image) def auto_delete_file_on_delete(sender, instance, **kwargs): """Deletes image from filesystem when corresponding object is deleted.""" @@ -107,6 +110,10 @@ def get_absolute_url(self): return reverse('zds.gallery.views.gallery_details', args=[self.pk, self.slug]) + def get_gallery_path(self): + """get the physical path to this gallery root""" + return os.path.join(MEDIA_ROOT, 'galleries', str(self.pk)) + # TODO rename function to get_users_galleries def get_users(self): return UserGallery.objects.all()\ @@ -121,3 +128,10 @@ def get_last_image(self): return Image.objects.all()\ .filter(gallery=self)\ .order_by('-pubdate')[0] + + +@receiver(models.signals.post_delete, sender=Gallery) +def auto_delete_image_on_delete(sender, instance, **kwargs): + """Deletes image from filesystem when corresponding object is deleted.""" + for image in instance.get_images(): + image.delete() diff --git a/zds/gallery/tests/tests_forms.py b/zds/gallery/tests/tests_forms.py index 3f8d5109e1..50a56111bb 100644 --- a/zds/gallery/tests/tests_forms.py +++ b/zds/gallery/tests/tests_forms.py @@ -5,7 +5,7 @@ from django.test import TestCase from django.core.files.uploadedfile import SimpleUploadedFile -from zds.gallery.forms import GalleryForm, UserGalleryForm, ImageForm, ImageAsAvatarForm +from zds.gallery.forms import GalleryForm, UserGalleryForm, ImageForm, ImageAsAvatarForm, ArchiveImageForm from zds.member.factories import ProfileFactory from zds import settings @@ -82,6 +82,18 @@ def test_valid_image_form(self): self.assertTrue(form.is_valid()) upload_file.close() + def test_valid_archive_image_form(self): + upload_file = open(os.path.join(settings.SITE_ROOT, 'fixtures', 'archive-gallery.zip'), 'r') + + data = {} + files = { + 'file': SimpleUploadedFile(upload_file.name, upload_file.read()) + } + form = ArchiveImageForm(data, files) + + self.assertTrue(form.is_valid()) + upload_file.close() + def test_empty_title_image_form(self): upload_file = open(os.path.join(settings.SITE_ROOT, 'fixtures', 'logo.png'), 'r') @@ -155,7 +167,7 @@ def test_too_long_title_image_form(self): files = { 'physical': SimpleUploadedFile(upload_file.name, upload_file.read()) } - form = ImageForm(data, files) + ImageForm(data, files) upload_file.close() def test_too_long_legend_image_form(self): @@ -169,7 +181,7 @@ def test_too_long_legend_image_form(self): files = { 'physical': SimpleUploadedFile(upload_file.name, upload_file.read()) } - form = ImageForm(data, files) + ImageForm(data, files) upload_file.close() diff --git a/zds/gallery/tests/tests_models.py b/zds/gallery/tests/tests_models.py index b3616b156f..33f81bf05b 100644 --- a/zds/gallery/tests/tests_models.py +++ b/zds/gallery/tests/tests_models.py @@ -104,7 +104,7 @@ def test_unicode(self): def test_get_absolute_url(self): absolute_url = reverse('zds.gallery.views.gallery_details', - args=[self.gallery.pk, self.gallery.slug]) + args=[self.gallery.pk, self.gallery.slug]) self.assertEqual(absolute_url, self.gallery.get_absolute_url()) def test_get_users(self): diff --git a/zds/gallery/tests/tests_views.py b/zds/gallery/tests/tests_views.py index 9f43e3559c..27ace2fa11 100644 --- a/zds/gallery/tests/tests_views.py +++ b/zds/gallery/tests/tests_views.py @@ -18,8 +18,8 @@ class GalleryListViewTest(TestCase): def test_denies_anonymous(self): response = self.client.get(reverse('zds.gallery.views.gallery_list'), follow=True) self.assertRedirects(response, - reverse('zds.member.views.login_view') - + '?next=' + urllib.quote(reverse('zds.gallery.views.gallery_list'), '')) + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.gallery.views.gallery_list'), '')) def test_list_galeries_belong_to_member(self): profile = ProfileFactory() @@ -28,8 +28,8 @@ def test_list_galeries_belong_to_member(self): UserGalleryFactory(user=profile.user, gallery=gallery) login_check = self.client.login( - username=profile.user.username, - password='hostel77' + username=profile.user.username, + password='hostel77' ) self.assertTrue(login_check) @@ -48,17 +48,17 @@ def setUp(self): def test_denies_anonymous(self): response = self.client.get(reverse('zds.gallery.views.gallery_details', - args=['89', 'test-gallery']), follow=True) + args=['89', 'test-gallery']), follow=True) self.assertRedirects(response, - reverse('zds.member.views.login_view') - + '?next=' + urllib.quote(reverse('zds.gallery.views.gallery_details', - args=['89', 'test-gallery']), '')) + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.gallery.views.gallery_details', + args=['89', 'test-gallery']), '')) def test_fail_gallery_no_exist(self): login_check = self.client.login(username=self.profile1.user.username, password='hostel77') self.assertTrue(login_check) response = self.client.get(reverse('zds.gallery.views.gallery_details', - args=['89', 'test-gallery']), follow=True) + args=['89', 'test-gallery']), follow=True) self.assertEqual(404, response.status_code) @@ -71,7 +71,7 @@ def test_fail_gallery_details_no_permission(self): self.assertTrue(login_check) response = self.client.get(reverse('zds.gallery.views.gallery_details', - args=[gallery.pk, gallery.slug])) + args=[gallery.pk, gallery.slug])) self.assertEqual(403, response.status_code) def test_success_gallery_details_permission_authorized(self): @@ -83,7 +83,7 @@ def test_success_gallery_details_permission_authorized(self): self.assertTrue(login_check) response = self.client.get(reverse('zds.gallery.views.gallery_details', - args=[gallery.pk, gallery.slug])) + args=[gallery.pk, gallery.slug])) self.assertEqual(200, response.status_code) @@ -95,15 +95,15 @@ def setUp(self): def test_denies_anonymous(self): response = self.client.get(reverse('zds.gallery.views.new_gallery'), follow=True) self.assertRedirects(response, - reverse('zds.member.views.login_view') + - '?next=' + - urllib.quote(reverse('zds.gallery.views.new_gallery'), '')) + reverse('zds.member.views.login_view') + + '?next=' + + urllib.quote(reverse('zds.gallery.views.new_gallery'), '')) response = self.client.post(reverse('zds.gallery.views.new_gallery'), follow=True) self.assertRedirects(response, - reverse('zds.member.views.login_view') + - '?next=' + - urllib.quote(reverse('zds.gallery.views.new_gallery'), '')) + reverse('zds.member.views.login_view') + + '?next=' + + urllib.quote(reverse('zds.gallery.views.new_gallery'), '')) def test_access_member(self): """ just verify with get request that everythings is ok """ @@ -172,12 +172,12 @@ def test_fail_delete_multi_read_permission(self): self.assertEqual(3, Image.objects.all().count()) response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'delete_multi': '', - 'items': [self.gallery1.pk] - }, - follow=True + reverse('zds.gallery.views.modify_gallery'), + { + 'delete_multi': '', + 'items': [self.gallery1.pk] + }, + follow=True ) self.assertEqual(403, response.status_code) @@ -194,12 +194,12 @@ def test_success_delete_multi_write_permission(self): self.assertEqual(3, Image.objects.all().count()) response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'delete_multi': '', - 'items': [self.gallery1.pk, self.gallery2.pk] - }, - follow=True + reverse('zds.gallery.views.modify_gallery'), + { + 'delete_multi': '', + 'items': [self.gallery1.pk, self.gallery2.pk] + }, + follow=True ) self.assertEqual(200, response.status_code) self.assertEqual(0, Gallery.objects.all().count()) @@ -212,23 +212,23 @@ def test_fail_add_user_with_read_permission(self): # gallery nonexistent response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'adduser': '', - 'gallery': 89, - } + reverse('zds.gallery.views.modify_gallery'), + { + 'adduser': '', + 'gallery': 89, + } ) self.assertEqual(404, response.status_code) # try to add an user with write permission response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'adduser': '', - 'gallery': self.gallery1.pk, - 'user': self.profile2.user.username, - 'mode': 'W', - } + reverse('zds.gallery.views.modify_gallery'), + { + 'adduser': '', + 'gallery': self.gallery1.pk, + 'user': self.profile2.user.username, + 'mode': 'W', + } ) self.assertEqual(403, response.status_code) @@ -238,14 +238,14 @@ def test_fail_add_user_already_has_permission(self): # Same permission : read response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'adduser': '', - 'gallery': self.gallery1.pk, - 'user': self.profile2.user.username, - 'mode': 'R', - }, - follow=True + reverse('zds.gallery.views.modify_gallery'), + { + 'adduser': '', + 'gallery': self.gallery1.pk, + 'user': self.profile2.user.username, + 'mode': 'R', + }, + follow=True ) self.assertEqual(200, response.status_code) permissions = UserGallery.objects.filter(user=self.profile2.user, gallery=self.gallery1) @@ -255,14 +255,14 @@ def test_fail_add_user_already_has_permission(self): # try to add write permission to an user # who has already an read permission response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'adduser': '', - 'gallery': self.gallery1.pk, - 'user': self.profile2.user.username, - 'mode': 'W', - }, - follow=True + reverse('zds.gallery.views.modify_gallery'), + { + 'adduser': '', + 'gallery': self.gallery1.pk, + 'user': self.profile2.user.username, + 'mode': 'W', + }, + follow=True ) self.assertEqual(200, response.status_code) permissions = UserGallery.objects.filter(user=self.profile2.user, gallery=self.gallery1) @@ -274,14 +274,14 @@ def test_success_add_user_read_permission(self): self.assertTrue(login_check) response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'adduser': '', - 'gallery': self.gallery1.pk, - 'user': self.profile3.user.username, - 'mode': 'R', - }, - follow=True + reverse('zds.gallery.views.modify_gallery'), + { + 'adduser': '', + 'gallery': self.gallery1.pk, + 'user': self.profile3.user.username, + 'mode': 'R', + }, + follow=True ) self.assertEqual(200, response.status_code) permissions = UserGallery.objects.filter(user=self.profile3.user, gallery=self.gallery1) @@ -293,14 +293,14 @@ def test_success_add_user_write_permission(self): self.assertTrue(login_check) response = self.client.post( - reverse('zds.gallery.views.modify_gallery'), - { - 'adduser': '', - 'gallery': self.gallery1.pk, - 'user': self.profile3.user.username, - 'mode': 'W', - }, - follow=True + reverse('zds.gallery.views.modify_gallery'), + { + 'adduser': '', + 'gallery': self.gallery1.pk, + 'user': self.profile3.user.username, + 'mode': 'W', + }, + follow=True ) self.assertEqual(200, response.status_code) permissions = UserGallery.objects.filter(user=self.profile3.user, gallery=self.gallery1) @@ -331,8 +331,8 @@ def test_denies_anonymous(self): follow=True ) self.assertRedirects(response, - reverse('zds.member.views.login_view') - + '?next=' + urllib.quote(reverse('zds.gallery.views.edit_image', args=[15, 156]), '')) + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.gallery.views.edit_image', args=[15, 156]), '')) def test_fail_member_no_permission_can_edit_image(self): login_check = self.client.login(username=self.profile3.user.username, password='hostel77') @@ -341,17 +341,17 @@ def test_fail_member_no_permission_can_edit_image(self): with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'logo.png'), 'r') as fp: self.client.post( - reverse( - 'zds.gallery.views.edit_image', - args=[self.gallery.pk, self.image.pk] - ), - { - 'title': 'modify with no perms', - 'legend': 'test legend', - 'slug': 'test-slug', - 'physical': fp - }, - follow=True + reverse( + 'zds.gallery.views.edit_image', + args=[self.gallery.pk, self.image.pk] + ), + { + 'title': 'modify with no perms', + 'legend': 'test legend', + 'slug': 'test-slug', + 'physical': fp + }, + follow=True ) image_test = Image.objects.get(pk=self.image.pk) @@ -361,7 +361,7 @@ def test_fail_member_no_permission_can_edit_image(self): def test_success_member_edit_image(self): login_check = self.client.login(username=self.profile1.user.username, password='hostel77') self.assertTrue(login_check) - + with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'logo.png'), 'r') as fp: response = self.client.post( @@ -417,19 +417,19 @@ def tearDown(self): def test_denies_anonymous(self): response = self.client.get(reverse('zds.gallery.views.delete_image'), follow=True) self.assertRedirects(response, - reverse('zds.member.views.login_view') - + '?next=' + urllib.quote(reverse('zds.gallery.views.delete_image'), '')) + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.gallery.views.delete_image'), '')) def test_fail_modify_image_with_no_permission(self): login_check = self.client.login(username=self.profile3.user.username, password='hostel77') self.assertTrue(login_check) response = self.client.post( - reverse('zds.gallery.views.delete_image'), - { - 'gallery': self.gallery1.pk, - }, - follow=True, + reverse('zds.gallery.views.delete_image'), + { + 'gallery': self.gallery1.pk, + }, + follow=True, ) self.assertTrue(403, response.status_code) @@ -445,13 +445,13 @@ def test_delete_image_from_other_user(self): self.assertTrue(login_check) self.client.post( - reverse('zds.gallery.views.delete_image'), - { - 'gallery': self.gallery1.pk, - 'delete': '', - 'image': image4.pk - }, - follow=True, + reverse('zds.gallery.views.delete_image'), + { + 'gallery': self.gallery1.pk, + 'delete': '', + 'image': image4.pk + }, + follow=True, ) self.assertEqual(1, Image.objects.filter(pk=image4.pk).count()) @@ -462,13 +462,13 @@ def test_success_delete_image_write_permission(self): self.assertTrue(login_check) response = self.client.post( - reverse('zds.gallery.views.delete_image'), - { - 'gallery': self.gallery1.pk, - 'delete': '', - 'image': self.image1.pk - }, - follow=True, + reverse('zds.gallery.views.delete_image'), + { + 'gallery': self.gallery1.pk, + 'delete': '', + 'image': self.image1.pk + }, + follow=True, ) self.assertEqual(200, response.status_code) @@ -479,13 +479,13 @@ def test_success_delete_list_images_write_permission(self): self.assertTrue(login_check) response = self.client.post( - reverse('zds.gallery.views.delete_image'), - { - 'gallery': self.gallery1.pk, - 'delete_multi': '', - 'items': [self.image1.pk, self.image2.pk] - }, - follow=True, + reverse('zds.gallery.views.delete_image'), + { + 'gallery': self.gallery1.pk, + 'delete_multi': '', + 'items': [self.image1.pk, self.image2.pk] + }, + follow=True, ) self.assertEqual(200, response.status_code) @@ -497,13 +497,13 @@ def test_fail_delete_image_read_permission(self): self.assertTrue(login_check) response = self.client.post( - reverse('zds.gallery.views.delete_image'), - { - 'gallery': self.gallery1.pk, - 'delete': '', - 'image': self.image1.pk - }, - follow=True, + reverse('zds.gallery.views.delete_image'), + { + 'gallery': self.gallery1.pk, + 'delete': '', + 'image': self.image1.pk + }, + follow=True, ) self.assertEqual(403, response.status_code) @@ -523,8 +523,8 @@ def setUp(self): def test_denies_anonymous(self): response = self.client.get(reverse('zds.gallery.views.new_image', args=[1]), follow=True) self.assertRedirects(response, - reverse('zds.member.views.login_view') - + '?next=' + urllib.quote(reverse('zds.gallery.views.new_image', args=[1]), '')) + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.gallery.views.new_image', args=[1]), '')) def test_success_new_image_write_permission(self): login_check = self.client.login(username=self.profile1.user.username, password='hostel77') @@ -533,19 +533,19 @@ def test_success_new_image_write_permission(self): with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'logo.png'), 'r') as fp: response = self.client.post( - reverse( - 'zds.gallery.views.new_image', - args=[self.gallery.pk] - ), - { - 'title': 'Test title', - 'legend': 'Test legend', - 'slug': 'test-slug', - 'physical': fp - }, - follow=True + reverse( + 'zds.gallery.views.new_image', + args=[self.gallery.pk] + ), + { + 'title': 'Test title', + 'legend': 'Test legend', + 'slug': 'test-slug', + 'physical': fp + }, + follow=True ) - + self.assertEqual(200, response.status_code) self.assertEqual(1, len(self.gallery.get_images())) self.gallery.get_images()[0].delete() @@ -557,17 +557,17 @@ def test_fail_new_image_with_read_permission(self): with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'logo.png'), 'r') as fp: response = self.client.post( - reverse( - 'zds.gallery.views.new_image', - args=[self.gallery.pk] - ), - { - 'title': 'Test title', - 'legend': 'Test legend', - 'slug': 'test-slug', - 'physical': fp - }, - follow=True + reverse( + 'zds.gallery.views.new_image', + args=[self.gallery.pk] + ), + { + 'title': 'Test title', + 'legend': 'Test legend', + 'slug': 'test-slug', + 'physical': fp + }, + follow=True ) self.assertEqual(403, response.status_code) @@ -580,17 +580,17 @@ def test_fail_new_image_with_no_permission(self): with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'logo.png'), 'r') as fp: response = self.client.post( - reverse( - 'zds.gallery.views.new_image', - args=[self.gallery.pk] - ), - { - 'title': 'Test title', - 'legend': 'Test legend', - 'slug': 'test-slug', - 'physical': fp - }, - follow=True + reverse( + 'zds.gallery.views.new_image', + args=[self.gallery.pk] + ), + { + 'title': 'Test title', + 'legend': 'Test legend', + 'slug': 'test-slug', + 'physical': fp + }, + follow=True ) self.assertEqual(403, response.status_code) @@ -602,17 +602,53 @@ def test_fail_gallery_not_exist(self): with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'logo.png'), 'r') as fp: response = self.client.post( - reverse( - 'zds.gallery.views.new_image', - args=[156] - ), - { - 'title': 'Test title', - 'legend': 'Test legend', - 'slug': 'test-slug', - 'physical': fp - }, - follow=True + reverse( + 'zds.gallery.views.new_image', + args=[156] + ), + { + 'title': 'Test title', + 'legend': 'Test legend', + 'slug': 'test-slug', + 'physical': fp + }, + follow=True ) self.assertEqual(404, response.status_code) + + def test_import_images_in_gallery(self): + login_check = self.client.login(username=self.profile1.user.username, password='hostel77') + self.assertTrue(login_check) + + with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'archive-gallery.zip'), 'r') as fp: + response = self.client.post( + reverse( + 'zds.gallery.views.import_image', + args=[self.gallery.pk] + ), + { + 'file': fp + }, + follow=False + ) + self.assertEqual(302, response.status_code) + self.assertEqual(Image.objects.filter(gallery=self.gallery).count(), 1) + + def test_denies_import_images_in_gallery(self): + login_check = self.client.login(username=self.profile2.user.username, password='hostel77') + self.assertTrue(login_check) + + with open(os.path.join(settings.SITE_ROOT, 'fixtures', 'archive-gallery.zip'), 'r') as fp: + response = self.client.post( + reverse( + 'zds.gallery.views.import_image', + args=[self.gallery.pk] + ), + { + 'file': fp + }, + follow=True + ) + self.assertEqual(403, response.status_code) + self.assertEqual(Image.objects.filter(gallery=self.gallery).count(), 0) diff --git a/zds/gallery/urls.py b/zds/gallery/urls.py index c343547b57..7a6b4dee3c 100644 --- a/zds/gallery/urls.py +++ b/zds/gallery/urls.py @@ -21,4 +21,6 @@ 'zds.gallery.views.delete_image'), url(r'^image/editer/(?P\d+)/(?P\d+)/$', 'zds.gallery.views.edit_image'), + url(r'^image/importer/(?P\d+)/$', + 'zds.gallery.views.import_image'), ) diff --git a/zds/gallery/views.py b/zds/gallery/views.py index 841ac9a037..51e711905c 100644 --- a/zds/gallery/views.py +++ b/zds/gallery/views.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from datetime import datetime +from PIL import Image as ImagePIL from django.conf import settings from django.contrib import messages from django.http import Http404 @@ -11,13 +12,19 @@ from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.shortcuts import redirect, get_object_or_404 -from zds.gallery.forms import ImageForm, UpdateImageForm, GalleryForm, UserGalleryForm, ImageAsAvatarForm +from zds.gallery.forms import ArchiveImageForm, ImageForm, UpdateImageForm, \ + GalleryForm, UserGalleryForm, ImageAsAvatarForm from zds.gallery.models import UserGallery, Image, Gallery -from zds.tutorial.models import Tutorial from zds.member.decorator import can_write_and_read_now from zds.utils import render_template from zds.utils import slugify +from django.core.files import File +from zds.tutorial.models import Tutorial +import zipfile +import shutil +import os +from django.db import transaction @login_required @@ -29,7 +36,6 @@ def gallery_list(request): {"galleries": galleries}) - @login_required def gallery_details(request, gal_pk, gal_slug): """Gallery details.""" @@ -94,16 +100,19 @@ def modify_gallery(request): if "delete_multi" in request.POST: l = request.POST.getlist("items") - + # Don't delete gallery when it's link to tutorial free_galleries = [] for g_pk in l: if Tutorial.objects.filter(gallery__pk=g_pk).exists(): gallery = Gallery.objects.get(pk=g_pk) - messages.error(request, "La galerie '{}' ne peut pas être supprimée car elle est liée à un tutoriel existant".format(gallery.title)) + messages.error( + request, + "La galerie '{}' ne peut pas être supprimée car elle est liée à un tutoriel existant".format( + gallery.title)) else: free_galleries.append(g_pk) - + perms = UserGallery.objects.filter(gallery__pk__in=free_galleries, user=request.user, mode="W").count() @@ -193,8 +202,10 @@ def edit_image(request, gal_pk, img_pk): if form.is_valid(): if "physical" in request.FILES: if request.FILES["physical"].size > settings.IMAGE_MAX_SIZE: - messages.error(request, "Votre image est beaucoup trop lourde, réduisez sa taille à moins de {} \ - Kio avant de l'envoyer".format(str(settings.IMAGE_MAX_SIZE/1024))) + messages.error(request, u"Votre image est beaucoup trop lourde, " + u"réduisez sa taille à moins de {} " + u"Kio " + u"avant de l'envoyer".format(str(settings.IMAGE_MAX_SIZE / 1024))) else: img.title = request.POST["title"] img.legend = request.POST["legend"] @@ -297,3 +308,82 @@ def new_image(request, gal_pk): form = ImageForm(initial={"new_image": True}) # A empty, unbound form return render_template("gallery/image/new.html", {"form": form, "gallery": gal}) + + +@can_write_and_read_now +@login_required +@transaction.atomic +def import_image(request, gal_pk): + """Create images from zip archive.""" + + gal = get_object_or_404(Gallery, pk=gal_pk) + + try: + gal_mode = UserGallery.objects.get(gallery=gal, user=request.user) + if gal_mode.mode != 'W': + raise PermissionDenied + except: + raise PermissionDenied + + # if request is POST + if request.method == "POST": + form = ArchiveImageForm(request.POST, request.FILES) + if form.is_valid(): + archive = request.FILES["file"] + temp = os.path.join(settings.SITE_ROOT, "temp") + if not os.path.exists(temp): + os.makedirs(temp) + zfile = zipfile.ZipFile(archive, "a") + for i in zfile.namelist(): + ph_temp = os.path.abspath(os.path.join(temp, i)) + (dirname, filename) = os.path.split(i) + # if directory doesn't exist, created on + if not os.path.exists(os.path.dirname(ph_temp)): + os.makedirs(os.path.dirname(ph_temp)) + # if file is directory, don't create file + if filename.strip() == "": + continue + data = zfile.read(i) + # create file for image + fp = open(ph_temp, "wb") + fp.write(data) + fp.close() + title = os.path.basename(i) + # if size is too large don't save + if os.stat(ph_temp).st_size > settings.IMAGE_MAX_SIZE: + messages.error( + request, + u"L'image {} n'a pas pu être importée dans la gallerie" + u" car elle est beaucoup trop lourde".format(title)) + continue + # if it's not an image, pass + try: + ImagePIL.open(ph_temp) + except IOError: + continue + f = File(open(ph_temp, "rb")) + f.name = title + # create picture in database + pic = Image() + pic.gallery = gal + pic.title = title + pic.pubdate = datetime.now() + pic.physical = f + pic.save() + f.close() + + zfile.close() + + if os.path.exists(temp): + shutil.rmtree(temp) + + # Redirect to the newly uploaded gallery + return redirect(reverse("zds.gallery.views.gallery_details", + args=[gal.pk, gal.slug])) + else: + return render_template("gallery/image/new.html", {"form": form, + "gallery": gal}) + else: + form = ArchiveImageForm(initial={"new_image": True}) # A empty, unbound form + return render_template("gallery/image/new.html", {"form": form, + "gallery": gal}) diff --git a/zds/member/decorator.py b/zds/member/decorator.py index c58474c3d1..fedf565b2f 100644 --- a/zds/member/decorator.py +++ b/zds/member/decorator.py @@ -1,6 +1,5 @@ # coding: utf-8 -from django.contrib.auth import logout from django.core.exceptions import PermissionDenied diff --git a/zds/member/forms.py b/zds/member/forms.py index 4eff50eacf..9e83d6359f 100644 --- a/zds/member/forms.py +++ b/zds/member/forms.py @@ -7,8 +7,6 @@ from django.contrib.auth.models import User, Group from django.core.urlresolvers import reverse -from email.utils import parseaddr - from crispy_forms.bootstrap import StrictButton from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Layout, \ @@ -49,7 +47,7 @@ def __init__(self, profile, *args, **kwargs): class LoginForm(forms.Form): username = forms.CharField( - label='Identifiant', + label='Nom d\'utilisateur', max_length=User._meta.get_field('username').max_length, required=True, widget=forms.TextInput( @@ -60,7 +58,7 @@ class LoginForm(forms.Form): ) password = forms.CharField( - label='Mot magique', + label='Mot de passe', max_length=MAX_PASSWORD_LENGTH, min_length=MIN_PASSWORD_LENGTH, required=True, @@ -68,7 +66,7 @@ class LoginForm(forms.Form): ) remember = forms.BooleanField( - label='Connexion automatique', + label='Se souvenir de moi', initial=True, ) @@ -85,7 +83,6 @@ def __init__(self, next=None, *args, **kwargs): HTML('{% csrf_token %}'), ButtonHolder( StrictButton('Se connecter', type='submit'), - HTML('Annuler'), ), ) @@ -132,7 +129,6 @@ def __init__(self, *args, **kwargs): Field('email'), ButtonHolder( Submit('submit', 'Valider mon inscription'), - HTML('Annuler'), )) def clean(self): @@ -155,8 +151,8 @@ def clean(self): # Check that the user doesn't exist yet username = cleaned_data.get('username') - - if username is not None : + + if username is not None: if username.strip() == '': msg = u'Le nom d\'utilisateur ne peut-être vide' self._errors['username'] = self.error_class([msg]) @@ -257,8 +253,7 @@ def __init__(self, *args, **kwargs): Field('avatar_url'), Field('sign'), ButtonHolder( - StrictButton('Éditer le profil', type='submit'), - HTML('Annuler'), + StrictButton(u'Enregistrer', type='submit'), )) @@ -272,7 +267,7 @@ class ProfileForm(MiniProfileForm): ('show_sign', "Afficher les signatures"), ('hover_or_click', "Cochez pour dérouler les menus au survol"), ('email_for_answer', u'Recevez un courriel lorsque vous ' - u'recevez une réponse à un message privé'), + u'recevez une réponse à un message privé'), ), widget=forms.CheckboxSelectMultiple, ) @@ -311,8 +306,7 @@ def __init__(self, *args, **kwargs): Field('sign'), Field('options'), ButtonHolder( - StrictButton('Editer mon profil', type='submit'), - HTML('Annuler'), + StrictButton(u'Enregistrer', type='submit'), )) @@ -340,7 +334,7 @@ class ChangeUserForm(forms.Form): 'placeholder': 'Ne mettez rien pour conserver l\'ancien' } ), - error_messages = {'invalid': u'Veuillez entrer une adresse email valide.',} + error_messages={'invalid': u'Veuillez entrer une adresse email valide.', } ) def __init__(self, *args, **kwargs): @@ -353,8 +347,7 @@ def __init__(self, *args, **kwargs): Field('username_new'), Field('email_new'), ButtonHolder( - StrictButton('Changer', type='submit'), - HTML('Annuler'), + StrictButton('Enregistrer', type='submit'), ), ) @@ -390,7 +383,7 @@ def clean(self): msg = u'Utilisez un autre fournisseur d\'adresses mail.' self._errors['email_new'] = self.error_class([msg]) break - + return cleaned_data @@ -430,8 +423,7 @@ def __init__(self, user, *args, **kwargs): Field('password_new'), Field('password_confirm'), ButtonHolder( - StrictButton('Changer', type='submit'), - HTML('Annuler'), + StrictButton('Enregistrer', type='submit'), ) ) @@ -497,7 +489,6 @@ def __init__(self, *args, **kwargs): Field('username'), ButtonHolder( StrictButton('Envoyer', type='submit'), - HTML('Annuler'), ) ) @@ -540,7 +531,6 @@ def __init__(self, identifier, *args, **kwargs): Field('password_confirm'), ButtonHolder( StrictButton('Envoyer', type='submit'), - HTML('Annuler'), ) ) @@ -581,10 +571,15 @@ class PromoteMemberForm(forms.Form): queryset=Group.objects.all(), required=False, ) - + superuser = forms.BooleanField( label="Super-user", - required=False, + required=False, + ) + + activation = forms.BooleanField( + label="Compte actif", + required=False, ) def __init__(self, *args, **kwargs): @@ -596,5 +591,6 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( Field('groups'), Field('superuser'), + Field('activation'), StrictButton('Valider', type='submit'), ) diff --git a/zds/member/models.py b/zds/member/models.py index 0b75e578ee..99d3478740 100644 --- a/zds/member/models.py +++ b/zds/member/models.py @@ -11,12 +11,14 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse +from django.dispatch import receiver import pygeoip from zds.article.models import Article from zds.forum.models import Post, Topic from zds.tutorial.models import Tutorial from zds.utils.models import Alert +from django.utils.importlib import import_module class Profile(models.Model): @@ -224,6 +226,12 @@ def get_followed_topics(self): .order_by('-last_message__pubdate') +@receiver(models.signals.post_delete, sender=User) +def auto_delete_token_on_unregistering(sender, instance, **kwargs): + TokenForgotPassword.objects.filter(user=instance).delete() + TokenRegister.objects.filter(user=instance).delete() + + class TokenForgotPassword(models.Model): class Meta: @@ -286,10 +294,12 @@ def logout_user(username): for session in sessions: user_id = session.get_decoded().get('_auth_user_id') if username == user_id: - request.session = init_session(session.session_key) + engine = import_module(settings.SESSION_ENGINE) + request.session = engine.SessionStore(session.session_key) logout(request) break + def listing(): fichier = [] diff --git a/zds/member/tests/tests_forms.py b/zds/member/tests/tests_forms.py index e91a293d04..ca7649ff0c 100644 --- a/zds/member/tests/tests_forms.py +++ b/zds/member/tests/tests_forms.py @@ -1,14 +1,10 @@ # coding: utf-8 - -from django.conf import settings -from django.contrib.auth.models import User -from django.core import mail from django.test import TestCase -from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.member.forms import OldTutoForm, LoginForm, RegisterForm, \ - MiniProfileForm, ProfileForm, ChangeUserForm, \ - ChangePasswordForm, ForgotPasswordForm, NewPasswordForm +from zds.member.factories import ProfileFactory +from zds.member.forms import LoginForm, RegisterForm, \ + MiniProfileForm, ProfileForm, ChangeUserForm, \ + ChangePasswordForm, ForgotPasswordForm, NewPasswordForm stringof77chars = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789-----" stringof129chars = u'http://www.01234567890123456789123456789' \ @@ -27,8 +23,9 @@ class LoginFormTest(TestCase): + """ Check the form to login """ - + def test_valid_login_form(self): data = { 'username': 'Tester', @@ -37,7 +34,7 @@ def test_valid_login_form(self): } form = LoginForm(data=data) self.assertTrue(form.is_valid()) - + def test_missing_username_form(self): data = { 'username': '', @@ -46,7 +43,7 @@ def test_missing_username_form(self): } form = LoginForm(data=data) self.assertFalse(form.is_valid()) - + def test_missing_password_form(self): data = { 'username': 'Tester', @@ -55,11 +52,12 @@ def test_missing_password_form(self): } form = LoginForm(data=data) self.assertFalse(form.is_valid()) - + class RegisterFormTest(TestCase): + """ Check the registering form """ - + def test_valid_register_form(self): data = { 'email': 'test@gmail.com', @@ -69,7 +67,7 @@ def test_valid_register_form(self): } form = RegisterForm(data=data) self.assertTrue(form.is_valid()) - + def test_empty_email_register_form(self): data = { 'email': '', @@ -79,7 +77,7 @@ def test_empty_email_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_empty_pseudo_register_form(self): data = { 'email': 'test@gmail.com', @@ -89,7 +87,7 @@ def test_empty_pseudo_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_empty_spaces_pseudo_register_form(self): data = { 'email': 'test@gmail.com', @@ -109,7 +107,7 @@ def test_forbiden_email_provider_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_not_matching_password_register_form(self): data = { 'email': 'test@gmail.com', @@ -119,7 +117,7 @@ def test_not_matching_password_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_too_short_password_register_form(self): data = { 'email': 'test@gmail.com', @@ -129,7 +127,7 @@ def test_too_short_password_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_too_long_password_register_form(self): data = { 'email': 'test@gmail.com', @@ -139,16 +137,6 @@ def test_too_long_password_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - - def test_password_match_pseudo_password_register_form(self): - data = { - 'email': 'test@gmail.com', - 'username': 'ZeTester', - 'password': 'ZeTester', - 'password_confirm': 'ZeTester' - } - form = RegisterForm(data=data) - self.assertFalse(form.is_valid()) def test_password_match_pseudo_password_register_form(self): data = { @@ -159,7 +147,7 @@ def test_password_match_pseudo_password_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_pseudo_exist_register_form(self): testuser = ProfileFactory() data = { @@ -170,7 +158,7 @@ def test_pseudo_exist_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_email_exist_register_form(self): testuser = ProfileFactory() data = { @@ -181,9 +169,9 @@ def test_email_exist_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_pseudo_espaces_register_form(self): - testuser = ProfileFactory() + ProfileFactory() data = { 'email': 'test@gmail.com', 'username': ' ZeTester ', @@ -192,9 +180,9 @@ def test_pseudo_espaces_register_form(self): } form = RegisterForm(data=data) self.assertFalse(form.is_valid()) - + def test_pseudo_coma_register_form(self): - testuser = ProfileFactory() + ProfileFactory() data = { 'email': 'test@gmail.com', 'username': 'Ze,Tester', @@ -206,11 +194,12 @@ def test_pseudo_coma_register_form(self): class MiniProfileFormTest(TestCase): + """ Check the miniprofile form """ - + def setUp(self): self.user1 = ProfileFactory() - + def test_valid_change_miniprofile_form(self): data = { 'biography': '', @@ -230,7 +219,7 @@ def test_too_long_site_url_miniprofile_form(self): } form = MiniProfileForm(data=data) self.assertFalse(form.is_valid()) - + def test_too_long_avatar_url_miniprofile_form(self): data = { 'biography': '', @@ -240,7 +229,7 @@ def test_too_long_avatar_url_miniprofile_form(self): } form = MiniProfileForm(data=data) self.assertFalse(form.is_valid()) - + def test_too_long_signature_miniprofile_form(self): data = { 'biography': '', @@ -253,8 +242,9 @@ def test_too_long_signature_miniprofile_form(self): class ProfileFormTest(TestCase): + """ Check the form is working (and that's all) """ - + def test_valid_profile_form(self): data = {} form = ProfileForm(data=data) @@ -262,11 +252,12 @@ def test_valid_profile_form(self): class ChangeUserFormTest(TestCase): + """ Check the user pseudo/email """ - + def setUp(self): self.user1 = ProfileFactory() - + def test_valid_change_pseudo_user_form(self): data = { 'username_new': "MyNewPseudo", @@ -274,7 +265,7 @@ def test_valid_change_pseudo_user_form(self): } form = ChangeUserForm(data=data) self.assertTrue(form.is_valid()) - + def test_valid_change_email_user_form(self): data = { 'username_new': '', @@ -282,7 +273,7 @@ def test_valid_change_email_user_form(self): } form = ChangeUserForm(data=data) self.assertTrue(form.is_valid()) - + def test_already_used_username_user_form(self): data = { 'username_new': self.user1.user.username, @@ -290,7 +281,7 @@ def test_already_used_username_user_form(self): } form = ChangeUserForm(data=data) self.assertFalse(form.is_valid()) - + def test_already_used_email_user_form(self): data = { 'username_new': '', @@ -298,7 +289,7 @@ def test_already_used_email_user_form(self): } form = ChangeUserForm(data=data) self.assertFalse(form.is_valid()) - + def test_forbidden_email_provider_user_form(self): data = { 'username_new': '', @@ -306,7 +297,7 @@ def test_forbidden_email_provider_user_form(self): } form = ChangeUserForm(data=data) self.assertFalse(form.is_valid()) - + def test_wrong_email_user_form(self): data = { 'username_new': '', @@ -344,7 +335,7 @@ def test_wrong_email_user_form(self): self.assertFalse(form.is_valid()) def test_pseudo_espaces_register_form(self): - testuser = ProfileFactory() + ProfileFactory() data = { 'username_new': ' ZeTester ', 'email_new': '' @@ -353,7 +344,7 @@ def test_pseudo_espaces_register_form(self): self.assertFalse(form.is_valid()) def test_pseudo_coma_register_form(self): - testuser = ProfileFactory() + ProfileFactory() data = { 'username_new': 'Ze,Tester', 'email_new': '' @@ -361,14 +352,16 @@ def test_pseudo_coma_register_form(self): form = ChangeUserForm(data=data) self.assertFalse(form.is_valid()) + class ChangePasswordFormTest(TestCase): + """ Check the form to change the password """ - + def setUp(self): self.user1 = ProfileFactory() self.oldpassword = "hostel77" self.newpassword = "TheNewPassword" - + def test_valid_change_password_form(self): data = { 'password_old': self.oldpassword, @@ -386,7 +379,7 @@ def test_old_wrong_change_password_form(self): } form = ChangePasswordForm(data=data, user=self.user1.user) self.assertFalse(form.is_valid()) - + def test_not_matching_change_password_form(self): data = { 'password_old': self.oldpassword, @@ -395,7 +388,7 @@ def test_not_matching_change_password_form(self): } form = ChangePasswordForm(data=data, user=self.user1.user) self.assertFalse(form.is_valid()) - + def test_too_short_change_password_form(self): tooshort = "short" data = { @@ -405,7 +398,7 @@ def test_too_short_change_password_form(self): } form = ChangePasswordForm(data=data, user=self.user1.user) self.assertFalse(form.is_valid()) - + def test_too_long_change_password_form(self): data = { 'password_old': self.oldpassword, @@ -414,7 +407,7 @@ def test_too_long_change_password_form(self): } form = ChangePasswordForm(data=data, user=self.user1.user) self.assertFalse(form.is_valid()) - + def test_match_pseudo_change_password_form(self): self.user1.user.username = "LongName" data = { @@ -427,25 +420,26 @@ def test_match_pseudo_change_password_form(self): class ForgotPasswordFormTest(TestCase): + """ Check the form to ask for a new password """ - + def setUp(self): self.user1 = ProfileFactory() - + def test_valid_forgot_password_form(self): data = { 'username': self.user1.user.username } form = ForgotPasswordForm(data=data) self.assertTrue(form.is_valid()) - + def test_empty_name_forgot_password_form(self): data = { 'username': '' } form = ForgotPasswordForm(data=data) self.assertFalse(form.is_valid()) - + def test_unknow_name_forgot_password_form(self): data = { 'username': "John Doe" @@ -455,12 +449,13 @@ def test_unknow_name_forgot_password_form(self): class NewPasswordFormTest(TestCase): + """ Check the form to input the new password """ - + def setUp(self): self.user1 = ProfileFactory() self.newpassword = "TheNewPassword" - + def test_valid_new_password_form(self): data = { 'password': self.newpassword, @@ -468,7 +463,7 @@ def test_valid_new_password_form(self): } form = NewPasswordForm(data=data, identifier=self.user1.user.username) self.assertTrue(form.is_valid()) - + def test_not_matching_new_password_form(self): data = { 'password': self.newpassword, @@ -476,7 +471,7 @@ def test_not_matching_new_password_form(self): } form = NewPasswordForm(data=data, identifier=self.user1.user.username) self.assertFalse(form.is_valid()) - + def test_password_is_username_new_password_form(self): data = { 'password': self.user1.user.username, @@ -484,7 +479,7 @@ def test_password_is_username_new_password_form(self): } form = NewPasswordForm(data=data, identifier=self.user1.user.username) self.assertFalse(form.is_valid()) - + def test_password_too_short_new_password_form(self): tooshort = "short" data = { @@ -493,7 +488,7 @@ def test_password_too_short_new_password_form(self): } form = NewPasswordForm(data=data, identifier=self.user1.user.username) self.assertFalse(form.is_valid()) - + def test_password_too_long_new_password_form(self): toolong = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789-----" data = { diff --git a/zds/member/tests/tests_models.py b/zds/member/tests/tests_models.py index 84467af323..a69bcc1055 100644 --- a/zds/member/tests/tests_models.py +++ b/zds/member/tests/tests_models.py @@ -3,11 +3,9 @@ import os import shutil -from datetime import datetime, timedelta +from datetime import datetime from django.conf import settings -from django.contrib.auth.models import User -from django.core import mail from django.test import TestCase from django.test.utils import override_settings from hashlib import md5 @@ -16,13 +14,13 @@ from zds.forum.factories import CategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.forum.models import TopicFollowed from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.member.models import Profile, TokenForgotPassword, TokenRegister +from zds.member.models import TokenForgotPassword, TokenRegister from zds.tutorial.factories import MiniTutorialFactory -from zds.tutorial.models import Validation from zds.gallery.factories import GalleryFactory -from zds.utils.models import Alert, Comment +from zds.utils.models import Alert from zds.settings import SITE_ROOT + @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) @override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) @override_settings( @@ -34,10 +32,11 @@ SITE_ROOT, 'articles-data-test')) class TestProfile(TestCase): + def setUp(self): self.user1 = ProfileFactory() self.staff = StaffProfileFactory() - + # Create a forum for later test self.forumcat = CategoryFactory() self.forum = ForumFactory(category=self.forumcat) @@ -48,20 +47,20 @@ def test_unicode(self): def test_get_absolute_url(self): self.assertEqual(self.user1.get_absolute_url(), '/membres/voir/{0}/'.format(self.user1.user.username)) - + # def test_get_city(self): - + def test_get_avatar_url(self): # if no url was specified -> gravatar ! self.assertEqual(self.user1.get_avatar_url(), 'https://secure.gravatar.com/avatar/{0}?d=identicon'. - format(md5(self.user1.user.email.lower()).hexdigest())) + format(md5(self.user1.user.email.lower()).hexdigest())) # if an url is specified -> take it ! user2 = ProfileFactory() testurl = 'http://test.com/avatar.jpg' user2.avatar_url = testurl self.assertEqual(user2.get_avatar_url(), testurl) - + def test_get_post_count(self): # Start with 0 self.assertEqual(self.user1.get_post_count(), 0) @@ -69,7 +68,7 @@ def test_get_post_count(self): PostFactory(topic=self.forumtopic, author=self.user1.user, position=1) # Should be 1 self.assertEqual(self.user1.get_post_count(), 1) - + def test_get_topic_count(self): # Start with 0 self.assertEqual(self.user1.get_topic_count(), 0) @@ -77,7 +76,7 @@ def test_get_topic_count(self): TopicFactory(forum=self.forum, author=self.user1.user) # Should be 1 self.assertEqual(self.user1.get_topic_count(), 1) - + def test_get_tuto_count(self): # Start with 0 self.assertEqual(self.user1.get_tuto_count(), 0) @@ -88,7 +87,7 @@ def test_get_tuto_count(self): minituto.save() # Should be 1 self.assertEqual(self.user1.get_tuto_count(), 1) - + def test_get_tutos(self): # Start with 0 self.assertEqual(len(self.user1.get_tutos()), 0) @@ -101,7 +100,7 @@ def test_get_tutos(self): tutos = self.user1.get_tutos() self.assertEqual(len(tutos), 1) self.assertEqual(minituto, tutos[0]) - + def test_get_draft_tutos(self): # Start with 0 self.assertEqual(len(self.user1.get_draft_tutos()), 0) @@ -114,7 +113,7 @@ def test_get_draft_tutos(self): drafttutos = self.user1.get_draft_tutos() self.assertEqual(len(drafttutos), 1) self.assertEqual(drafttuto, drafttutos[0]) - + def test_get_public_tutos(self): # Start with 0 self.assertEqual(len(self.user1.get_public_tutos()), 0) @@ -124,11 +123,11 @@ def test_get_public_tutos(self): publictuto.gallery = GalleryFactory() publictuto.sha_public = 'whatever' publictuto.save() - # Should be 1 + # Should be 1 publictutos = self.user1.get_public_tutos() self.assertEqual(len(publictutos), 1) self.assertEqual(publictuto, publictutos[0]) - + def test_get_validate_tutos(self): # Start with 0 self.assertEqual(len(self.user1.get_validate_tutos()), 0) @@ -142,7 +141,7 @@ def test_get_validate_tutos(self): validatetutos = self.user1.get_validate_tutos() self.assertEqual(len(validatetutos), 1) self.assertEqual(validatetuto, validatetutos[0]) - + def test_get_beta_tutos(self): # Start with 0 self.assertEqual(len(self.user1.get_beta_tutos()), 0) @@ -156,7 +155,7 @@ def test_get_beta_tutos(self): betatetutos = self.user1.get_beta_tutos() self.assertEqual(len(betatetutos), 1) self.assertEqual(betatetuto, betatetutos[0]) - + def test_get_articles(self): # Start with 0 self.assertEqual(len(self.user1.get_articles()), 0) @@ -168,7 +167,7 @@ def test_get_articles(self): articles = self.user1.get_articles() self.assertEqual(len(articles), 1) self.assertEqual(article, articles[0]) - + def test_get_public_articles(self): # Start with 0 self.assertEqual(len(self.user1.get_public_articles()), 0) @@ -181,7 +180,7 @@ def test_get_public_articles(self): articles = self.user1.get_public_articles() self.assertEqual(len(articles), 1) self.assertEqual(article, articles[0]) - + def test_get_validate_articles(self): # Start with 0 self.assertEqual(len(self.user1.get_validate_articles()), 0) @@ -194,7 +193,7 @@ def test_get_validate_articles(self): articles = self.user1.get_validate_articles() self.assertEqual(len(articles), 1) self.assertEqual(article, articles[0]) - + def test_get_draft_articles(self): # Start with 0 self.assertEqual(len(self.user1.get_draft_articles()), 0) @@ -207,7 +206,7 @@ def test_get_draft_articles(self): articles = self.user1.get_draft_articles() self.assertEqual(len(articles), 1) self.assertEqual(article, articles[0]) - + def test_get_posts(self): # Start with 0 self.assertEqual(len(self.user1.get_posts()), 0) @@ -217,7 +216,7 @@ def test_get_posts(self): posts = self.user1.get_posts() self.assertEqual(len(posts), 1) self.assertEqual(apost, posts[0]) - + def test_get_invisible_posts_count(self): # Start with 0 self.assertEqual(self.user1.get_invisible_posts_count(), 0) @@ -225,7 +224,7 @@ def test_get_invisible_posts_count(self): PostFactory(topic=self.forumtopic, author=self.user1.user, position=1, is_visible=False) # Should be 1 self.assertEqual(self.user1.get_invisible_posts_count(), 1) - + def test_get_alerts_posts_count(self): # Start with 0 self.assertEqual(self.user1.get_alerts_posts_count(), 0) @@ -234,23 +233,21 @@ def test_get_alerts_posts_count(self): Alert.objects.create(author=self.user1.user, comment=post, scope=Alert.FORUM, pubdate=datetime.now()) # Should be 1 self.assertEqual(self.user1.get_alerts_posts_count(), 1) - + def test_can_read_now(self): self.user1.user.is_active = False self.assertFalse(self.user1.can_write_now()) self.user1.user.is_active = True self.assertTrue(self.user1.can_write_now()) # TODO Some conditions still need to be tested - - + def test_can_write_now(self): self.user1.user.is_active = False self.assertFalse(self.user1.can_write_now()) self.user1.user.is_active = True self.assertTrue(self.user1.can_write_now()) # TODO Some conditions still need to be tested - - + def test_get_followed_topics(self): # Start with 0 self.assertEqual(len(self.user1.get_followed_topics()), 0) @@ -260,7 +257,7 @@ def test_get_followed_topics(self): topicsfollowed = self.user1.get_followed_topics() self.assertEqual(len(topicsfollowed), 1) self.assertEqual(self.forumtopic, topicsfollowed[0]) - + def tearDown(self): if os.path.isdir(settings.REPO_PATH): shutil.rmtree(settings.REPO_PATH) @@ -271,6 +268,7 @@ def tearDown(self): if os.path.isdir(settings.MEDIA_ROOT): shutil.rmtree(settings.MEDIA_ROOT) + class TestTokenForgotPassword(TestCase): def setUp(self): @@ -284,7 +282,7 @@ def test_get_absolute_url(self): class TestTokenRegister(TestCase): - + def setUp(self): self.user1 = ProfileFactory() self.token = TokenRegister.objects.create(user=self.user1.user, @@ -300,7 +298,7 @@ def test_unicode(self): # class TestBan(TestCase): # nothing to test ! - + # class TestDivers(TestCase): # logout_user diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index 9271b137e6..2e261dc352 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -1,20 +1,37 @@ # coding: utf-8 -import urllib - +import os from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.core import mail from django.core.urlresolvers import reverse from django.test import TestCase +from django.test.utils import override_settings -from zds.member.factories import ProfileFactory, StaffProfileFactory, NonAsciiProfileFactory -from zds.member.forms import RegisterForm, ChangeUserForm, ChangePasswordForm -from zds.member.models import Profile +from shutil import rmtree +from zds.settings import ANONYMOUS_USER, EXTERNAL_USER, SITE_ROOT +from zds.forum.models import TopicFollowed +from zds.member.factories import ProfileFactory, StaffProfileFactory, NonAsciiProfileFactory, UserFactory +from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +from zds.member.models import Profile +from zds.mp.models import PrivatePost, PrivateTopic from zds.member.models import TokenRegister, Ban +from zds.tutorial.factories import MiniTutorialFactory +from zds.tutorial.models import Tutorial, Validation +from zds.article.factories import ArticleFactory +from zds.article.models import Article +from zds.forum.factories import CategoryFactory, ForumFactory, TopicFactory, PostFactory +from zds.forum.models import Topic, Post +from zds.article.models import Validation as ArticleValidation +from zds.gallery.factories import GalleryFactory, UserGalleryFactory +from zds.gallery.models import Gallery, UserGallery +@override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) +@override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) +@override_settings(REPO_PATH_PROD=os.path.join(SITE_ROOT, 'tutoriels-public-test')) +@override_settings(REPO_ARTICLE_PATH=os.path.join(SITE_ROOT, 'articles-data-test')) class MemberTests(TestCase): def setUp(self): @@ -22,6 +39,13 @@ def setUp(self): 'django.core.mail.backends.locmem.EmailBackend' self.mas = ProfileFactory() settings.BOT_ACCOUNT = self.mas.user.username + self.anonymous = UserFactory(username=ANONYMOUS_USER, password="anything") + self.external = UserFactory(username=EXTERNAL_USER, password="anything") + self.category1 = CategoryFactory(position=1) + self.forum11 = ForumFactory( + category=self.category1, + position_in_category=1) + self.staff = StaffProfileFactory().user def test_login(self): """To test user login.""" @@ -76,6 +100,271 @@ def test_register(self): self.assertTrue(User.objects.get(username='firm1').is_active) + def test_unregister(self): + """Tests that unregistering user is working""" + + # test not logged user can't unregister + self.client.logout() + result = self.client.post( + reverse('zds.member.views.unregister'), + follow=False) + self.assertEqual(result.status_code, 302) + user = ProfileFactory() + login_check = self.client.login( + username=user.user.username, + password='hostel77') + self.assertEqual(login_check, True) + result = self.client.post( + reverse('zds.member.views.unregister'), + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(User.objects.filter(username=user.user.username).count(), 0) + user = ProfileFactory() + user2 = ProfileFactory() + aloneGallery = GalleryFactory() + UserGalleryFactory(gallery=aloneGallery, user=user.user) + sharedGallery = GalleryFactory() + UserGalleryFactory(gallery=sharedGallery, user=user.user) + UserGalleryFactory(gallery=sharedGallery, user=user2.user) + # first case : a published tutorial with only one author + publishedTutorialAlone = MiniTutorialFactory(light=True) + publishedTutorialAlone.authors.add(user.user) + publishedTutorialAlone.save() + # second case : a published tutorial with two authors + publishedTutorial2 = MiniTutorialFactory(light=True) + publishedTutorial2.authors.add(user.user) + publishedTutorial2.authors.add(user2.user) + publishedTutorial2.save() + # third case : a private tutorial with only one author + writingTutorialAlone = MiniTutorialFactory(light=True) + writingTutorialAlone.authors.add(user.user) + writingTutorialAlone.save() + writingTutorialAloneGallerPath = writingTutorialAlone.gallery.get_gallery_path() + writingTutorialAlonePath = writingTutorialAlone.get_path() + # fourth case : a private tutorial with at least two authors + writingTutorial2 = MiniTutorialFactory(light=True) + writingTutorial2.authors.add(user.user) + writingTutorial2.authors.add(user2.user) + writingTutorial2.save() + self.client.login(username=self.staff.username, password="hostel77") + pub = self.client.post( + reverse('zds.tutorial.views.ask_validation'), + { + 'tutorial': publishedTutorialAlone.pk, + 'text': u'Ce tuto est excellent', + 'version': publishedTutorialAlone.sha_draft, + 'source': 'http://zestedesavoir.com', + }, + follow=False) + self.assertEqual(pub.status_code, 302) + # reserve tutorial + validation = Validation.objects.get( + tutorial__pk=publishedTutorialAlone.pk) + pub = self.client.post( + reverse('zds.tutorial.views.reservation', args=[validation.pk]), + follow=False) + self.assertEqual(pub.status_code, 302) + # publish tutorial + pub = self.client.post( + reverse('zds.tutorial.views.valid_tutorial'), + { + 'tutorial': publishedTutorialAlone.pk, + 'text': u'Ce tuto est excellent', + 'is_major': True, + 'source': 'http://zestedesavoir.com', + }, + follow=False) + pub = self.client.post( + reverse('zds.tutorial.views.ask_validation'), + { + 'tutorial': publishedTutorial2.pk, + 'text': u'Ce tuto est excellent', + 'version': publishedTutorial2.sha_draft, + 'source': 'http://zestedesavoir.com', + }, + follow=False) + self.assertEqual(pub.status_code, 302) + # reserve tutorial + validation = Validation.objects.get( + tutorial__pk=publishedTutorial2.pk) + pub = self.client.post( + reverse('zds.tutorial.views.reservation', args=[validation.pk]), + follow=False) + self.assertEqual(pub.status_code, 302) + # publish tutorial + pub = self.client.post( + reverse('zds.tutorial.views.valid_tutorial'), + { + 'tutorial': publishedTutorial2.pk, + 'text': u'Ce tuto est excellent', + 'is_major': True, + 'source': 'http://zestedesavoir.com', + }, + follow=False) + # same thing for articles + publishedArticleAlone = ArticleFactory() + publishedArticleAlone.authors.add(user.user) + publishedArticleAlone.save() + publishedArticle2 = ArticleFactory() + publishedArticle2.authors.add(user.user) + publishedArticle2.authors.add(user2.user) + publishedArticle2.save() + + writingArticleAlone = ArticleFactory() + writingArticleAlone.authors.add(user.user) + writingArticleAlone.save() + writingArticle2 = ArticleFactory() + writingArticle2.authors.add(user.user) + writingArticle2.authors.add(user2.user) + writingArticle2.save() + # ask public article + pub = self.client.post( + reverse('zds.article.views.modify'), + { + 'article': publishedArticleAlone.pk, + 'comment': u'Valides moi ce bébé', + 'pending': 'Demander validation', + 'version': publishedArticleAlone.sha_draft, + 'is_major': True + }, + follow=False) + self.assertEqual(pub.status_code, 302) + + login_check = self.client.login( + username=self.staff.username, + password='hostel77') + self.assertEqual(login_check, True) + + # reserve article + validation = ArticleValidation.objects.get( + article__pk=publishedArticleAlone.pk) + pub = self.client.post( + reverse('zds.article.views.reservation', args=[validation.pk]), + follow=False) + self.assertEqual(pub.status_code, 302) + + # publish article + pub = self.client.post( + reverse('zds.article.views.modify'), + { + 'article': publishedArticleAlone.pk, + 'comment-v': u'Cet article est excellent', + 'valid-article': 'Demander validation', + 'is_major': True + }, + follow=False) + self.assertEqual(pub.status_code, 302) + # ask public article + pub = self.client.post( + reverse('zds.article.views.modify'), + { + 'article': publishedArticle2.pk, + 'comment': u'Valides moi ce bébé', + 'pending': 'Demander validation', + 'version': publishedArticle2.sha_draft, + 'is_major': True + }, + follow=False) + self.assertEqual(pub.status_code, 302) + + login_check = self.client.login( + username=self.staff.username, + password='hostel77') + self.assertEqual(login_check, True) + + # reserve article + validation = ArticleValidation.objects.get( + article__pk=publishedArticle2.pk) + pub = self.client.post( + reverse('zds.article.views.reservation', args=[validation.pk]), + follow=False) + self.assertEqual(pub.status_code, 302) + + # publish article + pub = self.client.post( + reverse('zds.article.views.modify'), + { + 'article': publishedArticle2.pk, + 'comment-v': u'Cet article est excellent', + 'valid-article': 'Demander validation', + 'is_major': True + }, + follow=False) + self.assertEqual(pub.status_code, 302) + # about posts and topics + authoredTopic = TopicFactory(author=user.user, forum=self.forum11) + answeredTopic = TopicFactory(author=user2.user, forum=self.forum11) + PostFactory(topic=answeredTopic, author=user.user, position=2) + editedAnswer = PostFactory(topic=answeredTopic, author=user.user, position=3) + editedAnswer.editor = user.user + editedAnswer.save() + privateTopic = PrivateTopicFactory(author=user.user) + privateTopic.participants.add(user2.user) + privateTopic.save() + PrivatePostFactory(author=user.user, privatetopic=privateTopic, position_in_topic=1) + login_check = self.client.login( + username=user.user.username, + password='hostel77') + self.assertEqual(login_check, True) + result = self.client.post( + reverse('zds.member.views.unregister'), + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(publishedTutorialAlone.authors.count(), 1) + self.assertEqual(publishedTutorialAlone.authors.first().username, EXTERNAL_USER) + self.assertFalse(os.path.exists(writingTutorialAloneGallerPath)) + self.assertEqual(publishedTutorial2.authors.count(), 1) + self.assertEqual(publishedTutorial2.authors.filter(username=EXTERNAL_USER).count(), 0) + self.assertIsNotNone(publishedTutorial2.get_prod_path()) + self.assertTrue(os.path.exists(publishedTutorial2.get_prod_path())) + self.assertIsNotNone(publishedTutorialAlone.get_prod_path()) + self.assertTrue(os.path.exists(publishedTutorialAlone.get_prod_path())) + self.assertEqual(self.client.get( + reverse('zds.tutorial.views.view_tutorial_online', args=[ + publishedTutorialAlone.pk, + publishedTutorialAlone.slug]), follow=False).status_code, 200) + self.assertEqual(self.client.get( + reverse('zds.tutorial.views.view_tutorial_online', args=[ + publishedTutorial2.pk, + publishedTutorial2.slug]), follow=False).status_code, 200) + self.assertTrue(os.path.exists(publishedArticleAlone.get_path())) + self.assertEqual(self.client.get( + reverse( + 'zds.article.views.view_online', + args=[ + publishedArticleAlone.pk, + publishedArticleAlone.slug]), + follow=True).status_code, 200) + self.assertEqual(self.client.get( + reverse( + 'zds.article.views.view_online', + args=[ + publishedArticle2.pk, + publishedArticle2.slug]), + follow=True).status_code, 200) + self.assertEqual(Tutorial.objects.filter(pk=writingTutorialAlone.pk).count(), 0) + self.assertEqual(writingTutorial2.authors.count(), 1) + self.assertEqual(writingTutorial2.authors.filter(username=EXTERNAL_USER).count(), 0) + self.assertEqual(publishedArticleAlone.authors.count(), 1) + self.assertEqual(publishedArticleAlone.authors.first().username, EXTERNAL_USER) + self.assertEqual(publishedArticle2.authors.count(), 1) + self.assertEqual(publishedArticle2.authors.filter(username=EXTERNAL_USER).count(), 0) + self.assertEqual(Article.objects.filter(pk=writingArticleAlone.pk).count(), 0) + self.assertEqual(writingArticle2.authors.count(), 1) + self.assertEqual(writingArticle2.authors.filter(username=EXTERNAL_USER).count(), 0) + self.assertEqual(Topic.objects.filter(author__username=user.user.username).count(), 0) + self.assertEqual(Post.objects.filter(author__username=user.user.username).count(), 0) + self.assertEqual(Post.objects.filter(editor__username=user.user.username).count(), 0) + self.assertEqual(PrivatePost.objects.filter(author__username=user.user.username).count(), 0) + self.assertEqual(PrivateTopic.objects.filter(author__username=user.user.username).count(), 0) + self.assertFalse(os.path.exists(writingTutorialAlonePath)) + self.assertIsNotNone(Topic.objects.get(pk=authoredTopic.pk)) + self.assertIsNotNone(PrivateTopic.objects.get(pk=privateTopic.pk)) + self.assertIsNotNone(Gallery.objects.get(pk=aloneGallery.pk)) + self.assertEquals(aloneGallery.get_users().count(), 1) + self.assertEquals(sharedGallery.get_users().count(), 1) + self.assertEquals(UserGallery.objects.filter(user=user.user).count(), 0) + def test_sanctions(self): """Test various sanctions.""" @@ -202,7 +491,144 @@ def test_sanctions(self): def test_nonascii(self): user = NonAsciiProfileFactory() result = self.client.get(reverse('zds.member.views.login_view') + '?next=' - + reverse('zds.member.views.details', args=[user.user.username]), + + reverse('zds.member.views.details', args=[user.user.username]), follow=False) self.assertEqual(result.status_code, 200) + def test_promote_interface(self): + """Test promotion interface""" + + # create users (one regular and one staff and superuser) + tester = ProfileFactory() + staff = StaffProfileFactory() + tester.user.is_active = False + tester.user.save() + staff.user.is_superuser = True + staff.user.save() + + # create groups + group = Group.objects.create(name="DummyGroup_1") + groupbis = Group.objects.create(name="DummyGroup_2") + + # create Forums, Posts and subscribe member to them + category1 = CategoryFactory(position=1) + forum1 = ForumFactory( + category=category1, + position_in_category=1) + forum1.group.add(group) + forum1.save() + forum2 = ForumFactory( + category=category1, + position_in_category=2) + forum2.group.add(groupbis) + forum2.save() + forum3 = ForumFactory( + category=category1, + position_in_category=3) + topic1 = TopicFactory(forum=forum1, author=staff.user) + topic2 = TopicFactory(forum=forum2, author=staff.user) + topic3 = TopicFactory(forum=forum3, author=staff.user) + + # LET THE TEST BEGIN ! + + # tester shouldn't be able to connect + login_check = self.client.login( + username=tester.user.username, + password='hostel77') + self.assertEqual(login_check, False) + + # connect as staff (superuser) + login_check = self.client.login( + username=staff.user.username, + password='hostel77') + self.assertEqual(login_check, True) + + # check that we can go through the page + result = self.client.get( + reverse('zds.member.views.settings_promote', + kwargs={'user_pk': tester.user.id}), follow=False) + self.assertEqual(result.status_code, 200) + + # give user rights and groups thanks to staff (but account still not activated) + result = self.client.post( + reverse('zds.member.views.settings_promote', + kwargs={'user_pk': tester.user.id}), + { + 'groups': [group.id, groupbis.id], + 'superuser': "on", + }, follow=False) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(id=tester.id) # refresh + + self.assertEqual(len(tester.user.groups.all()), 2) + self.assertFalse(tester.user.is_active) + self.assertTrue(tester.user.is_superuser) + + # Now our tester is going to follow one post in every forum (3) + TopicFollowed(topic=topic1, user=tester.user).save() + TopicFollowed(topic=topic2, user=tester.user).save() + TopicFollowed(topic=topic3, user=tester.user).save() + + self.assertEqual(TopicFollowed.objects.filter(user=tester.user).count(), 3) + + # retract all right, keep one group only and activate account + result = self.client.post( + reverse('zds.member.views.settings_promote', + kwargs={'user_pk': tester.user.id}), + { + 'groups': [group.id], + 'activation': "on" + }, follow=False) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(id=tester.id) # refresh + + self.assertEqual(len(tester.user.groups.all()), 1) + self.assertTrue(tester.user.is_active) + self.assertFalse(tester.user.is_superuser) + self.assertEqual(TopicFollowed.objects.filter(user=tester.user).count(), 2) + + # no groups specified + result = self.client.post( + reverse('zds.member.views.settings_promote', + kwargs={'user_pk': tester.user.id}), + { + 'activation': "on" + }, follow=False) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(id=tester.id) # refresh + self.assertEqual(TopicFollowed.objects.filter(user=tester.user).count(), 1) + + # check that staff can't take away it's own super user rights + result = self.client.post( + reverse('zds.member.views.settings_promote', + kwargs={'user_pk': staff.user.id}), + { + 'activation': "on" + }, follow=False) + self.assertEqual(result.status_code, 302) + staff = Profile.objects.get(id=staff.id) # refresh + self.assertTrue(staff.user.is_superuser) # still superuser ! + + # Finally, check that user can connect and can not access the interface + login_check = self.client.login( + username=tester.user.username, + password='hostel77') + self.assertEqual(login_check, True) + result = self.client.post( + reverse('zds.member.views.settings_promote', + kwargs={'user_pk': staff.user.id}), + { + 'activation': "on" + }, follow=False) + self.assertEqual(result.status_code, 403) # forbidden ! + + def tearDown(self): + Profile.objects.all().delete() + if os.path.isdir(settings.REPO_ARTICLE_PATH): + rmtree(settings.REPO_ARTICLE_PATH) + if os.path.isdir(settings.MEDIA_ROOT): + rmtree(settings.MEDIA_ROOT) + if os.path.isdir(settings.REPO_PATH): + rmtree(settings.REPO_PATH) + if os.path.isdir(settings.REPO_PATH_PROD): + rmtree(settings.REPO_PATH_PROD) diff --git a/zds/member/urls.py b/zds/member/urls.py index 1a7957750e..9932d50f08 100644 --- a/zds/member/urls.py +++ b/zds/member/urls.py @@ -7,6 +7,8 @@ urlpatterns = patterns('', url(r'^$', 'zds.member.views.index'), + url(r'^desinscrire/valider/$', 'zds.member.views.unregister'), + url(r'^desinscrire/avertissement/$', 'zds.member.views.warning_unregister'), url(r'^voir/(?P.+)/$', 'zds.member.views.details'), url(r'^profil/modifier/(?P\d+)/$', @@ -20,8 +22,6 @@ 'zds.member.views.tutorials'), url(r'^articles/$', 'zds.member.views.articles'), - url(r'^actions/$', - 'zds.member.views.actions'), url(r'^parametres/profil/$', 'zds.member.views.settings_profile'), @@ -50,4 +50,4 @@ 'zds.member.views.active_account'), url(r'^envoi_jeton/$', 'zds.member.views.generate_token_account'), - ) \ No newline at end of file + ) diff --git a/zds/member/views.py b/zds/member/views.py index c458ba5902..a735cd7773 100644 --- a/zds/member/views.py +++ b/zds/member/views.py @@ -8,7 +8,7 @@ from django.contrib import messages from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import User, Group, Permission, SiteProfileNotAvailable +from django.contrib.auth.models import User, Group, SiteProfileNotAvailable from django.core.context_processors import csrf from django.core.exceptions import PermissionDenied from django.core.mail import EmailMultiAlternatives @@ -18,9 +18,13 @@ from django.db.models import Q from django.http import Http404, HttpResponse from django.shortcuts import redirect, get_object_or_404 -from django.template import Context, RequestContext +from django.template import Context from django.template.loader import get_template from django.views.decorators.http import require_POST +from zds.settings import ANONYMOUS_USER, EXTERNAL_USER +from zds.utils.models import Comment +from zds.mp.models import PrivatePost, PrivateTopic +from zds.gallery.models import UserGallery import json import pygal @@ -31,7 +35,7 @@ get_info_old_tuto, logout_user from zds.gallery.forms import ImageAsAvatarForm from zds.article.models import Article -from zds.forum.models import Topic, follow +from zds.forum.models import Topic, follow, TopicFollowed from zds.member.decorator import can_write_and_read_now from zds.tutorial.models import Tutorial from zds.utils import render_template @@ -45,7 +49,7 @@ def index(request): if request.is_ajax(): q = request.GET.get('q', '') - if request.user.is_authenticated() : + if request.user.is_authenticated(): members = User.objects.filter(username__icontains=q).exclude(pk=request.user.pk)[:20] else: members = User.objects.filter(username__icontains=q)[:20] @@ -57,9 +61,9 @@ def index(request): member_json['value'] = member.username results.append(member_json) data = json.dumps(results) - + mimetype = "application/json" - + return HttpResponse(data, mimetype) else: @@ -85,6 +89,99 @@ def index(request): }) +@login_required +def warning_unregister(request): + """displays a warning page showing what will happen when user unregisters""" + return render_template("member/settings/unregister.html", {"user": request.user}) + + +@login_required +@require_POST +@transaction.atomic +def unregister(request): + """allow members to unregister""" + + anonymous = get_object_or_404(User, username=ANONYMOUS_USER) + external = get_object_or_404(User, username=EXTERNAL_USER) + current = request.user + for tuto in request.user.profile.get_tutos(): + # we delete article only if not published with only one author + if not tuto.on_line() and tuto.authors.count() == 1: + if tuto.in_beta(): + beta_topic = Topic.objects.get(key=tuto.pk) + first_post = beta_topic.first_post() + first_post.update_content(u'# Le tutoriel présenté par ce topic n\'existe plus.') + tuto.delete_entity_and_tree() + else: + if tuto.authors.count() == 1: + tuto.authors.add(external) + external_gallery = UserGallery() + external_gallery.user = external + external_gallery.gallery = tuto.gallery + external_gallery.mode = 'W' + external_gallery.save() + UserGallery.objects.filter(user=current).filter(gallery=tuto.gallery).delete() + + tuto.authors.remove(current) + tuto.save() + for article in request.user.profile.get_articles(): + # we delete article only if not published with only one author + if not article.on_line() and article.authors.count() == 1: + article.delete_entity_and_tree() + else: + if article.authors.count() == 1: + article.authors.add(external) + article.authors.remove(current) + article.save() + # all messages anonymisation (forum, article and tutorial posts) + for message in Comment.objects.filter(author=current): + message.author = anonymous + message.save() + for message in PrivatePost.objects.filter(author=current): + message.author = anonymous + message.save() + # in case current has been moderator in his old day + for message in Comment.objects.filter(editor=current): + message.editor = anonymous + message.save() + for topic in PrivateTopic.objects.filter(author=current): + topic.participants.remove(current) + if topic.participants.count() > 0: + topic.author = topic.participants.first() + topic.participants.remove(topic.author) + topic.save() + else: + topic.delete() + for topic in PrivateTopic.objects.filter(participants__in=[current]): + topic.participants.remove(current) + topic.save() + for topic in Topic.objects.filter(author=current): + topic.author = anonymous + topic.save() + TopicFollowed.objects.filter(user=current).delete() + # Before deleting gallery let's summurize what we deleted + # - unpublished tutorials with only the unregistering member as an author + # - unpublished articles with only the unregistering member as an author + # - all category associated with those entites (have a look on article.delete_entity_and_tree + # and tutorial.delete_entity_and_tree + # So concerning galleries, we just have for us + # - "personnal galleries" with only one owner (unregistering user) + # - "personnal galleries" with more than one owner + # so we will just delete the unretistering user ownership and give it to anonymous in the only case + # he was alone so that gallery is not lost + for gallery in UserGallery.objects.filter(user=current): + if gallery.gallery.get_users().count() == 1: + anonymousGallery = UserGallery() + anonymousGallery.user = external + anonymousGallery.mode = "w" + anonymousGallery.gallery = gallery.gallery + anonymousGallery.save() + gallery.delete() + + logout(request) + User.objects.filter(pk=current.pk).delete() + return redirect(reverse("zds.pages.views.home")) + def details(request, user_name): """Displays details about a profile.""" @@ -125,7 +222,7 @@ def details(request, user_name): .filter(authors__in=[usr]) \ .order_by("-pubdate" ).all()[:5] - + my_tuto_versions = [] for my_tutorial in my_tutorials: mandata = my_tutorial.load_json_for_public() @@ -181,7 +278,7 @@ def modify_profile(request, user_pk): ban.type = u"Lecture Seule" ban.text = request.POST["ls-text"] detail = (u'Vous ne pouvez plus poster dans les forums, ni dans les ' - u'commentaires d\'articles et de tutoriels.') + u'commentaires d\'articles et de tutoriels.') if "ls-temp" in request.POST: ban.type = u"Lecture Seule Temporaire" ban.text = request.POST["ls-temp-text"] @@ -190,8 +287,8 @@ def modify_profile(request, user_pk): + timedelta(days=int(request.POST["ls-jrs"]), hours=0, minutes=0, seconds=0) detail = (u'Vous ne pouvez plus poster dans les forums, ni dans les ' - u'commentaires d\'articles et de tutoriels pendant {0} jours.' - .format(request.POST["ls-jrs"])) + u'commentaires d\'articles et de tutoriels pendant {0} jours.' + .format(request.POST["ls-jrs"])) if "ban-temp" in request.POST: ban.type = u"Ban Temporaire" ban.text = request.POST["ban-temp-text"] @@ -200,7 +297,7 @@ def modify_profile(request, user_pk): + timedelta(days=int(request.POST["ban-jrs"]), hours=0, minutes=0, seconds=0) detail = (u'Vous ne pouvez plus vous connecter sur Zeste de Savoir ' - u'pendant {0} jours.'.format(request.POST["ban-jrs"])) + u'pendant {0} jours.'.format(request.POST["ban-jrs"])) logout_user(profile.user.username) if "ban" in request.POST: @@ -214,7 +311,7 @@ def modify_profile(request, user_pk): ban.text = request.POST["unls-text"] profile.can_write = True detail = (u'Vous pouvez désormais poster sur les forums, dans les ' - u'commentaires d\'articles et tutoriels.') + u'commentaires d\'articles et tutoriels.') if "un-ban" in request.POST: ban.type = u"Autorisation de se connecter" ban.text = request.POST["unban-text"] @@ -233,10 +330,10 @@ def modify_profile(request, user_pk): u'Le motif de votre sanction est :\n\n' u'> {3}\n\n' u'Cordialement, L\'équipe Zeste de Savoir.' - .format(ban.user, - ban.moderator, - detail, - ban.text)) + .format(ban.user, + ban.moderator, + detail, + ban.text)) else: msg = (u'Bonjour **{0}**,\n\n' u'Vous avez été santionné par **{1}**.\n\n' @@ -244,11 +341,11 @@ def modify_profile(request, user_pk): u'Le motif de votre sanction est :\n\n' u'> {4}\n\n' u'Cordialement, L\'équipe Zeste de Savoir.' - .format(ban.user, - ban.moderator, - ban.type, - detail, - ban.text)) + .format(ban.user, + ban.moderator, + ban.type, + detail, + ban.text)) bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) send_mp( bot, @@ -262,7 +359,6 @@ def modify_profile(request, user_pk): return redirect(profile.get_absolute_url()) - @login_required def tutorials(request): """Returns all tutorials of the authenticated user.""" @@ -293,7 +389,6 @@ def tutorials(request): {"tutorials": user_tutorials, "type": type}) - @login_required def articles(request): """Returns all articles of the authenticated user.""" @@ -322,22 +417,6 @@ def articles(request): {"articles": user_articles, "type": type}) - -@login_required -def actions(request): - """Show avaible actions for current user, like a customized homepage. - - This may be very temporary. - - """ - - # TODO: Seriously improve this page, and see if cannot be merged in - # zds.pages.views.home since it will be more coherent to give an enhanced - # homepage for registered users - - return render_template("member/actions.html") - - # settings for public profile @can_write_and_read_now @@ -489,7 +568,6 @@ def settings_account(request): def settings_user(request): """User's settings about his email.""" - profile = request.user.profile if request.method == "POST": form = ChangeUserForm(request.POST) c = {"form": form} @@ -599,10 +677,10 @@ def register_view(request): # Generate a valid token during one hour. - uuidToken = str(uuid.uuid4()) + uuid_token = str(uuid.uuid4()) date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0) - token = TokenRegister(user=user, token=uuidToken, + token = TokenRegister(user=user, token=uuid_token, date_end=date_end) token.save() @@ -628,7 +706,6 @@ def register_view(request): return render_template("member/register/index.html", {"form": form}) - def forgot_password(request): """If the user forgot his password, he can have a new one.""" @@ -641,10 +718,10 @@ def forgot_password(request): # Generate a valid token during one hour. - uuidToken = str(uuid.uuid4()) + uuid_token = str(uuid.uuid4()) date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0) - token = TokenForgotPassword(user=usr, token=uuidToken, + token = TokenForgotPassword(user=usr, token=uuid_token, date_end=date_end) token.save() @@ -668,7 +745,6 @@ def forgot_password(request): return render_template("member/forgot_password/index.html", {"form": form}) - def new_password(request): """Create a new password for a user.""" @@ -764,8 +840,9 @@ def active_account(request): True, False, ) - return render_template("member/register/token_success.html", {"usr": usr}) token.delete() + form = LoginForm(initial={'username': usr.username}) + return render_template("member/register/token_success.html", {"usr": usr, "form": form}) def generate_token_account(request): @@ -830,7 +907,6 @@ def date_to_chart(posts): return lst - @login_required @require_POST def add_oldtuto(request): @@ -853,7 +929,6 @@ def add_oldtuto(request): args=[profile.user.username])) - @login_required def remove_oldtuto(request): if "id" in request.GET: @@ -895,26 +970,26 @@ def settings_promote(request, user_pk): profile = get_object_or_404(Profile, user__pk=user_pk) user = profile.user - + if request.method == "POST": form = PromoteMemberForm(request.POST) data = dict(form.data.iterlists()) groups = Group.objects.all() usergroups = user.groups.all() - + if 'groups' in data: for group in groups: if unicode(group.id) in data['groups']: if group not in usergroups: user.groups.add(group) messages.success(request, u'{0} appartient maintenant au groupe {1}' - .format(user.username, group.name)) + .format(user.username, group.name)) else: if group in usergroups: user.groups.remove(group) messages.warning(request, u'{0} n\'appartient maintenant plus au groupe {1}' - .format(user.username, group.name)) + .format(user.username, group.name)) topics_followed = Topic.objects.filter(topicfollowed__user=user, forum__group=group) for topic in topics_followed: @@ -927,13 +1002,13 @@ def settings_promote(request, user_pk): follow(topic, user) user.groups.clear() messages.warning(request, u'{0} n\'appartient (plus ?) à aucun groupe' - .format(user.username)) - + .format(user.username)) + if 'superuser' in data and u'on' in data['superuser']: if not user.is_superuser: user.is_superuser = True messages.success(request, u'{0} est maintenant super-utilisateur' - .format(user.username)) + .format(user.username)) else: if user == request.user: messages.error(request, u'Un super-utilisateur ne peux pas se retirer des super-utilisateur') @@ -941,21 +1016,30 @@ def settings_promote(request, user_pk): if user.is_superuser: user.is_superuser = False messages.warning(request, u'{0} n\'est maintenant plus super-utilisateur' - .format(user.username)) + .format(user.username)) + + if 'activation' in data and u'on' in data['activation']: + user.is_active = True + messages.success(request, u'{0} est maintenant activé' + .format(user.username)) + else: + user.is_active = False + messages.warning(request, u'{0} est désactivé' + .format(user.username)) user.save() - + usergroups = user.groups.all() bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) msg = (u'Bonjour {0},\n\n' u'Un administrateur vient de modifier les groupes ' u'auxquels vous appartenez. \n'.format(user.username)) if len(usergroups) > 0: - msg += u'Voici la liste des groupes dont vous faites dorénavant partis :\n\n' + msg += u'Voici la liste des groupes dont vous faites dorénavant partie :\n\n' for group in usergroups: msg += u'* {0}\n'.format(group.name) else: - msg += u'* Vous ne faites partis d\'aucun groupe' + msg += u'* Vous ne faites partie d\'aucun groupe' msg += u'\n\n' if user.is_superuser: msg += (u'Vous avez aussi rejoint le rang des super utilisateurs. ' @@ -969,15 +1053,15 @@ def settings_promote(request, user_pk): True, True, ) - + return redirect(profile.get_absolute_url()) form = PromoteMemberForm(initial={'superuser': user.is_superuser, - 'groups': user.groups.all() - }) - + 'groups': user.groups.all(), + 'activation': user.is_active + }) return render_template('member/settings/promote.html', { "usr": user, "profile": profile, "form": form - }) + }) diff --git a/zds/middlewares/SetLastVisitMiddleware.py b/zds/middlewares/SetLastVisitMiddleware.py index ae59aaa63b..1c6d923e48 100644 --- a/zds/middlewares/SetLastVisitMiddleware.py +++ b/zds/middlewares/SetLastVisitMiddleware.py @@ -1,6 +1,7 @@ import datetime from zds.member.views import get_client_ip + class SetLastVisitMiddleware(object): def process_response(self, request, response): diff --git a/zds/middlewares/profile.py b/zds/middlewares/profile.py index 349dfd2bd4..ebdcb038b2 100644 --- a/zds/middlewares/profile.py +++ b/zds/middlewares/profile.py @@ -5,18 +5,19 @@ import sys import os import re -import hotshot, hotshot.stats +import hotshot +import hotshot.stats import tempfile import StringIO from django.conf import settings -words_re = re.compile( r'\s+' ) +words_re = re.compile(r'\s+') group_prefix_re = [ - re.compile( "^.*/django/[^/]+" ), - re.compile( "^(.*)/[^/]+$" ), # extract module path - re.compile( ".*" ), # catch strange entries + re.compile("^.*/django/[^/]+"), + re.compile("^(.*)/[^/]+$"), # extract module path + re.compile(".*"), # catch strange entries ] @@ -25,6 +26,7 @@ def can_profile(request): class ProfileMiddleware(object): + """ Displays hotshot profiling for any view. http://yoursite.com/yourview/?prof @@ -48,18 +50,18 @@ def process_view(self, request, callback, callback_args, callback_kwargs): def get_group(self, file): for g in group_prefix_re: - name = g.findall( file ) + name = g.findall(file) if name: return name[0] def get_summary(self, results_dict, sum): - list = [ (item[1], item[0]) for item in results_dict.items() ] - list.sort( reverse = True ) + list = [(item[1], item[0]) for item in results_dict.items()] + list.sort(reverse=True) list = list[:40] res = " tottime\n" for item in list: - res += "%4.1f%% %7.3f %s\n" % ( 100*item[0]/sum if sum else 0, item[0], item[1] ) + res += "%4.1f%% %7.3f %s\n" % (100 * item[0] / sum if sum else 0, item[0], item[1]) return res @@ -72,24 +74,24 @@ def summary_for_files(self, stats_str): sum = 0 for s in stats_str: - fields = words_re.split(s); + fields = words_re.split(s) if len(fields) == 7: time = float(fields[2]) sum += time file = fields[6].split(":")[0] - if not file in mystats: + if file not in mystats: mystats[file] = 0 mystats[file] += time group = self.get_group(file) - if not group in mygroups: - mygroups[ group ] = 0 - mygroups[ group ] += time + if group not in mygroups: + mygroups[group] = 0 + mygroups[group] += time return "
    " + \
    -               " ---- By file ----\n\n" + self.get_summary(mystats,sum) + "\n" + \
    -               " ---- By group ---\n\n" + self.get_summary(mygroups,sum) + \
    +               " ---- By file ----\n\n" + self.get_summary(mystats, sum) + "\n" + \
    +               " ---- By group ---\n\n" + self.get_summary(mygroups, sum) + \
                    "
    " def process_response(self, request, response): diff --git a/zds/mp/forms.py b/zds/mp/forms.py index a6174579ff..fec9bb6ff1 100644 --- a/zds/mp/forms.py +++ b/zds/mp/forms.py @@ -75,7 +75,7 @@ def clean(self): if participants is not None and participants.strip() != '': receivers = participants.strip().split(',') for receiver in receivers: - if User.objects.filter(username=receiver.strip()).count() == 0 and receiver.strip() != '': + if User.objects.filter(username__exact=receiver.strip()).count() == 0 and receiver.strip() != '': self._errors['participants'] = self.error_class( [u'Un des participants saisi est introuvable']) elif receiver.strip() == self.username: diff --git a/zds/mp/models.py b/zds/mp/models.py index bf7b76e5b8..bfda8979f7 100644 --- a/zds/mp/models.py +++ b/zds/mp/models.py @@ -62,12 +62,15 @@ def first_post(self): .order_by('pubdate')\ .first() - def last_read_post(self): + def last_read_post(self, user=None): """Return the last private post the user has read.""" + if user is None: + user = get_current_user() + try: post = PrivateTopicRead.objects\ .select_related()\ - .filter(privatetopic=self, user=get_current_user()) + .filter(privatetopic=self, user=user) if len(post) == 0: return self.first_post() else: @@ -76,13 +79,16 @@ def last_read_post(self): except PrivatePost.DoesNotExist: return self.first_post() - def first_unread_post(self): + def first_unread_post(self, user=None): """Return the first post the user has unread.""" + if user is None: + user = get_current_user() + try: last_post = PrivateTopicRead.objects\ .select_related()\ - .filter(privatetopic=self, user=get_current_user())\ - .latest('post__pubdate').privatepost + .filter(privatetopic=self, user=user)\ + .latest('privatepost__pubdate').privatepost next_post = PrivatePost.objects.filter( privatetopic__pk=self.pk, @@ -96,8 +102,11 @@ def alone(self): """Check if there just one participant in the conversation.""" return self.participants.count() == 0 - def never_read(self): - return never_privateread(self) + def never_read(self, user=None): + if user is None: + user = get_current_user() + + return never_privateread(self, user) class PrivatePost(models.Model): @@ -159,6 +168,7 @@ def __unicode__(self): def never_privateread(privatetopic, user=None): """Check if a private topic has been read by an user since it last post was added.""" + if user is None: user = get_current_user() @@ -168,15 +178,19 @@ def never_privateread(privatetopic, user=None): .count() == 0 -def mark_read(privatetopic): +def mark_read(privatetopic, user=None): """Mark a private topic as read for the user.""" + + if user is None: + user = get_current_user() + PrivateTopicRead.objects.filter( privatetopic=privatetopic, - user=get_current_user()).delete() + user=user).delete() t = PrivateTopicRead( privatepost=privatetopic.last_message, privatetopic=privatetopic, - user=get_current_user()) + user=user) t.save() diff --git a/zds/mp/tests.py b/zds/mp/tests.py deleted file mode 100644 index 5431fb9919..0000000000 --- a/zds/mp/tests.py +++ /dev/null @@ -1,315 +0,0 @@ -# coding: utf-8 - -from django.core import mail -from django.core.urlresolvers import reverse -from django.test import TestCase - -from zds import settings -from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory -from zds.mp.models import PrivateTopic, PrivatePost -from zds.utils import slugify - - -class MPTests(TestCase): - - def setUp(self): - self.user1 = ProfileFactory().user - self.staff = StaffProfileFactory().user - log = self.client.login( - username=self.user1.username, - password='hostel77') - self.assertEqual(log, True) - - settings.EMAIL_BACKEND = \ - 'django.core.mail.backends.locmem.EmailBackend' - - def test_mp_from_profile(self): - """Test: Send a MP from a user profile.""" - # User to send the MP - user2 = ProfileFactory().user - - # Test if user is correctly added to the MP - result = self.client.get( - reverse('zds.mp.views.new') + - '?username={0}'.format( - user2.username), - ) - - # Check username in new MP page - self.assertContains(result, user2.username) - - def test_mp_multiple_users_from_link(self): - """Test: Send a MP to multiple users by link.""" - # Users to send the MP - user2 = ProfileFactory().user - user3 = ProfileFactory().user - - # Test if user is correctly added to the MP - result = self.client.get( - reverse('zds.mp.views.new') + - '?username={0}&username={1}'.format( - user2.username, - user3.username - ), - ) - - # Check username in new MP page - self.assertContains(result, user2.username) - self.assertContains(result, user3.username) - - def test_view_mp(self): - """check mp is readable.""" - ptopic1 = PrivateTopicFactory(author=self.user1) - PrivatePostFactory( - privatetopic=ptopic1, - author=self.user1, - position_in_topic=1) - PrivatePostFactory( - privatetopic=ptopic1, - author=self.staff, - position_in_topic=2) - PrivatePostFactory( - privatetopic=ptopic1, - author=self.user1, - position_in_topic=3) - - result = self.client.get( - reverse( - 'zds.mp.views.topic', - args=[ - ptopic1.pk, - slugify(ptopic1.title)]), - follow=True) - self.assertEqual(result.status_code, 200) - - def test_create_mp(self): - """To test all aspects of mp's creation by member.""" - # Another User - user2 = ProfileFactory().user - user3 = ProfileFactory().user - - result = self.client.post( - reverse('zds.mp.views.new'), - { - 'participants': '{0}, {1}'.format(user2.username, - user3.username), - 'title': u'Un autre MP', - 'subtitle': u'Encore ces lombards en plein été', - 'text': u'C\'est tout simplement l\'histoire de la ville de Paris que je voudrais vous conter ' - }, - follow=False) - self.assertEqual(result.status_code, 302) - - # check topic's number - self.assertEqual(PrivateTopic.objects.all().count(), 1) - ptopic = PrivateTopic.objects.get(pk=1) - # check post's number - self.assertEqual(PrivatePost.objects.all().count(), 1) - ppost = PrivatePost.objects.get(pk=1) - - # check topic and post - self.assertEqual(ppost.privatetopic, ptopic) - - # check position - self.assertEqual(ppost.position_in_topic, 1) - - self.assertEqual(ppost.author, self.user1) - - # check last message - self.assertEqual(ptopic.last_message, ppost) - - # check email has been sent - self.assertEquals(len(mail.outbox), 2) - - # check view authorisations - user4 = ProfileFactory().user - staff1 = StaffProfileFactory().user - - # user2 and user3 can view mp - self.client.login(username=user2.username, password='hostel77') - result = self.client.get( - reverse( - 'zds.mp.views.topic', - args=[ - ptopic.pk, - ptopic.pk]), - follow=True) - self.assertEqual(result.status_code, 200) - self.client.login(username=user3.username, password='hostel77') - result = self.client.get( - reverse( - 'zds.mp.views.topic', - args=[ - ptopic.pk, - ptopic.pk]), - follow=True) - self.assertEqual(result.status_code, 200) - - # user4 and staff1 can't view mp - self.client.login(username=user4.username, password='hostel77') - result = self.client.get( - reverse( - 'zds.mp.views.topic', - args=[ - ptopic.pk, - ptopic.pk]), - follow=True) - self.assertNotEqual(result.status_code, 200) - self.client.login(username=staff1.username, password='hostel77') - result = self.client.get( - reverse( - 'zds.mp.views.topic', - args=[ - ptopic.pk, - ptopic.pk]), - follow=True) - self.assertNotEqual(result.status_code, 200) - - def test_edit_mp_post(self): - """To test all aspects of the edition of simple mp post by member.""" - - ptopic1 = PrivateTopicFactory(author=self.user1) - ppost1 = PrivatePostFactory( - privatetopic=ptopic1, - author=self.user1, - position_in_topic=1) - ppost2 = PrivatePostFactory( - privatetopic=ptopic1, - author=self.user1, - position_in_topic=2) - ppost3 = PrivatePostFactory( - privatetopic=ptopic1, - author=self.user1, - position_in_topic=3) - - result = self.client.post( - reverse('zds.mp.views.edit_post') + '?message={0}' - .format(ppost3.pk), - { - 'text': u'C\'est tout simplement l\'histoire de la ville de Paris que je voudrais vous conter ' - }, - follow=False) - - self.assertEqual(result.status_code, 302) - - # check topic's number - self.assertEqual(PrivateTopic.objects.all().count(), 1) - - # check post's number - self.assertEqual(PrivatePost.objects.all().count(), 3) - - # check topic and post - self.assertEqual(ppost1.privatetopic, ptopic1) - self.assertEqual(ppost2.privatetopic, ptopic1) - self.assertEqual(ppost3.privatetopic, ptopic1) - - # check values - self.assertEqual( - PrivatePost.objects.get( - pk=ppost3.pk).text, - u"C\'est tout simplement l\'histoire de la ville de Paris que je voudrais vous conter ") - - # check no email has been sent - self.assertEquals(len(mail.outbox), 0) - - # i can edit a mp if it's not last - result = self.client.post( - reverse('zds.mp.views.edit_post') + '?message={0}' - .format(ppost2.pk), - { - 'text': u"C\'est tout simplement l\'histoire de la ville de Paris que je voudrais vous conter " - }, - follow=False) - - self.assertEqual(result.status_code, 403) - - # staff can't edit mp if he's not author - staff = StaffProfileFactory().user - log = self.client.login(username=staff.username, password='hostel77') - self.assertEqual(log, True) - - result = self.client.post( - reverse('zds.mp.views.edit_post') + '?message={0}' - .format(ppost3.pk), - { - 'text': u'C\'est tout simplement l\'histoire de la ville de Paris que je voudrais vous conter ' - }, - follow=False) - - self.assertEqual(result.status_code, 403) - - def test_delete_mp_post(self): - """To test all aspects of the deletion of simple mp post by member.""" - - # Another User - user2 = ProfileFactory().user - user3 = ProfileFactory().user - - result = self.client.post( - reverse('zds.mp.views.new'), - { - 'participants': '{0}, {1}'.format(user2.username, - user3.username), - 'title': u'Un autre MP', - 'subtitle': u'Encore ces lombards en plein été', - 'text': u'C\'est tout simplement l\'histoire de la ville de Paris que je voudrais vous conter ' - }, - follow=False) - self.assertEqual(result.status_code, 302) - - # check topic's number - self.assertEqual(PrivateTopic.objects.all().count(), 1) - ptopic = PrivateTopic.objects.get(pk=1) - - # check post's number - self.assertEqual(PrivatePost.objects.all().count(), 1) - ppost = PrivatePost.objects.get(pk=1) - - # check author of the MP - self.assertEqual(ppost.author, self.user1) - - # User3 would like leave the MP. He isn't the author - # and there will be still users in the MP. - self.client.login(username=user3.username, password='hostel77') - result = self.client.post( - reverse('zds.mp.views.leave'), - { - 'leave': 'leave', - 'topic_pk': ptopic.pk, - }, - follow=False) - self.assertEqual(result.status_code, 302) - - # check there are still 2 participants in MP. - self.assertEqual(ptopic.participants.count(), 1) - - # User1 would like leave the MP. He is the author so User2 - # will become the new author of the MP. - self.client.login(username=self.user1.username, password='hostel77') - result = self.client.post( - reverse('zds.mp.views.leave'), - { - 'leave': 'leave', - 'topic_pk': ptopic.pk, - }, - follow=False) - self.assertEqual(result.status_code, 302) - - # check there is still 1 participant in MP. - self.assertEqual(ptopic.participants.count(), 0) - - # User2 would like leave the MP. He is the author but there - # isn't other participants, the MP must to be delete. - self.client.login(username=user2.username, password='hostel77') - result = self.client.post( - reverse('zds.mp.views.leave'), - { - 'leave': 'leave', - 'topic_pk': ptopic.pk, - }, - follow=False) - self.assertEqual(result.status_code, 302) - - # check there is no more MP. - self.assertEqual(PrivateTopic.objects.all().count(), 0) diff --git a/zds/mp/tests/__init__.py b/zds/mp/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/mp/tests/tests_forms.py b/zds/mp/tests/tests_forms.py new file mode 100644 index 0000000000..1640203d49 --- /dev/null +++ b/zds/mp/tests/tests_forms.py @@ -0,0 +1,147 @@ +# coding: utf-8 + +from django.test import TestCase + +from zds.member.factories import ProfileFactory, StaffFactory +from zds.mp.forms import PrivateTopicForm, PrivatePostForm +from zds.mp.factories import PrivateTopicFactory + + +class PrivateTopicFormTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.staff1 = StaffFactory() + + def test_valid_topic_form(self): + data = { + 'participants': + self.profile1.user.username + + ',' + self.staff1.username, + 'title': 'Test title', + 'subtitle': 'Test subtitle', + 'text': 'blabla' + } + + form = PrivateTopicForm(self.profile2.user.username, data=data) + + self.assertTrue(form.is_valid()) + + def test_invalid_topic_form_user_notexist(self): + + data = { + 'participants': self.profile1.user.username + ', toto, tata', + 'title': 'Test title', + 'subtitle': 'Test subtitle', + 'text': 'blabla' + } + + form = PrivateTopicForm(self.profile1.user.username, data=data) + + self.assertFalse(form.is_valid()) + + def test_invalid_topic_form_no_participants(self): + + data = { + 'title': 'Test title', + 'subtitle': 'Test subtitle', + 'text': 'blabla' + } + + form = PrivateTopicForm('', data=data) + + self.assertFalse(form.is_valid()) + + def test_invalid_topic_form_empty_participants(self): + + data = { + 'participants': ' ', + 'title': 'Test title', + 'subtitle': 'Test subtitle', + 'text': 'blabla' + } + + form = PrivateTopicForm(' ', data=data) + + self.assertFalse(form.is_valid()) + + def test_invalid_topic_form_no_title(self): + + data = { + 'participants': self.profile1.user.username, + 'subtitle': 'Test subtitle', + 'text': 'blabla' + } + + form = PrivateTopicForm(self.profile1.user.username, data=data) + + self.assertFalse(form.is_valid()) + + def test_invalid_topic_form_empty_title(self): + + data = { + 'participants': self.profile1.user.username, + 'title': ' ', + 'subtitle': 'Test subtitle', + 'text': 'blabla' + } + + form = PrivateTopicForm(self.profile1.user.username, data=data) + + self.assertFalse(form.is_valid()) + + def test_invalid_topic_form_no_text(self): + + data = { + 'participants': self.profile1.user.username, + 'title': 'Test title', + 'subtitle': 'Test subtitle', + } + + form = PrivateTopicForm(self.profile1.user.username, data=data) + + self.assertFalse(form.is_valid()) + + def test_invalid_topic_form_empty_text(self): + + data = { + 'participants': self.profile1.user.username, + 'title': 'Test title', + 'subtitle': 'Test subtitle', + 'text': ' ' + } + + form = PrivateTopicForm(self.profile1.user.username, data=data) + + self.assertFalse(form.is_valid()) + + +class PrivatePostFormTest(TestCase): + + def setUp(self): + self.profile = ProfileFactory() + self.topic = PrivateTopicFactory(author=self.profile.user) + + def test_valid_form_post(self): + data = { + 'text': 'blabla' + } + + form = PrivatePostForm(self.topic, self.profile.user, data=data) + + self.assertTrue(form.is_valid()) + + def test_invalid_form_post_empty_text(self): + data = { + 'text': ' ' + } + + form = PrivatePostForm(self.topic, self.profile.user, data=data) + + self.assertFalse(form.is_valid()) + + def test_invalid_form_post_no_text(self): + form = PrivatePostForm(self.topic, self.profile.user, data={}) + + self.assertFalse(form.is_valid()) diff --git a/zds/mp/tests/tests_models.py b/zds/mp/tests/tests_models.py new file mode 100644 index 0000000000..5954ab8a74 --- /dev/null +++ b/zds/mp/tests/tests_models.py @@ -0,0 +1,231 @@ +# coding: utf-8 + +from django.test import TestCase +from django.core.urlresolvers import reverse +from math import ceil + +from zds.member.factories import ProfileFactory +from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +from zds.mp.models import mark_read, never_privateread +from zds.utils import slugify +from zds import settings + +# by moment, i wrote the scenario to be simpler + + +class PrivateTopicTest(TestCase): + + def setUp(self): + # scenario - topic1 : + # post1 - user1 - unread + # post2 - user2 - unread + + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + def test_unicode(self): + self.assertEqual(self.topic1.__unicode__(), self.topic1.title) + + def test_absolute_url(self): + url = reverse( + 'zds.mp.views.topic', + args=[self.topic1.pk, slugify(self.topic1.title)]) + + self.assertEqual(self.topic1.get_absolute_url(), url) + + def test_post_count(self): + self.assertEqual(2, self.topic1.get_post_count()) + + def test_get_last_answer(self): + topic = PrivateTopicFactory(author=self.profile2.user) + PrivatePostFactory( + privatetopic=topic, + author=self.profile2.user, + position_in_topic=1) + + self.assertEqual(self.post2, self.topic1.get_last_answer()) + self.assertNotEqual(self.post1, self.topic1.get_last_answer()) + + self.assertIsNone(topic.get_last_answer()) + + def test_first_post(self): + topic = PrivateTopicFactory(author=self.profile2.user) + self.assertEqual(self.post1, self.topic1.first_post()) + self.assertIsNone(topic.first_post()) + + def test_last_read_post(self): + # scenario - topic1 : + # post1 - user1 - unread + # post2 - user2 - unread + self.assertEqual( + self.post1, + self.topic1.last_read_post(self.profile1.user)) + + # scenario - topic1 : + # post1 - user1 - read + # post2 - user2 - read + mark_read(self.topic1, user=self.profile1.user) + self.assertEqual( + self.post2, + self.topic1.last_read_post(self.profile1.user)) + + # scenario - topic1 : + # post1 - user1 - read + # post2 - user2 - read + # post3 - user2 - unread + PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=3) + self.assertEqual( + self.post2, + self.topic1.last_read_post(self.profile1.user)) + + def test_first_unread_post(self): + # scenario - topic1 : + # post1 - user1 - unread + # post2 - user2 - unread + self.assertEqual( + self.post1, + self.topic1.first_unread_post(self.profile1.user)) + + # scenario - topic1 : + # post1 - user1 - read + # post2 - user2 - read + # post3 - user2 - unread + mark_read(self.topic1, self.profile1.user) + post3 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=3) + + self.assertEqual( + post3, + self.topic1.first_unread_post(self.profile1.user)) + + def test_alone(self): + topic2 = PrivateTopicFactory(author=self.profile1.user) + self.assertFalse(self.topic1.alone()) + self.assertTrue(topic2.alone()) + + def test_never_read(self): + # scenario - topic1 : + # post1 - user1 - unread + # post2 - user2 - unread + self.assertTrue(self.topic1.never_read(self.profile1.user)) + + # scenario - topic1 : + # post1 - user1 - read + # post2 - user2 - read + mark_read(self.topic1, self.profile1.user) + self.assertFalse(self.topic1.never_read(self.profile1.user)) + + # scenario - topic1 : + # post1 - user1 - read + # post2 - user2 - read + # post3 - user2 - unread + PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=3) + + self.assertTrue(self.topic1.never_read(self.profile1.user)) + + +class PrivatePostTest(TestCase): + + def setUp(self): + # scenario - topic1 : + # post1 - user1 - unread + # post2 - user2 - unread + + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + def test_unicode(self): + title = u''.format( + self.post1.privatetopic, + self.post1.pk) + self.assertEqual(title, self.post1.__unicode__()) + + def test_absolute_url(self): + page = int( + ceil( + float( + self.post1.position_in_topic) / + settings.POSTS_PER_PAGE)) + + url = '{0}?page={1}#p{2}'.format( + self.post1.privatetopic.get_absolute_url(), + page, + self.post1.pk) + + self.assertEqual(url, self.post1.get_absolute_url()) + + +class FunctionTest(TestCase): + + def setUp(self): + # scenario - topic1 : + # post1 - user1 - unread + # post2 - user2 - unread + + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + def test_never_privateread(self): + self.assertTrue(never_privateread(self.topic1, self.profile1.user)) + mark_read(self.topic1, self.profile1.user) + self.assertFalse(never_privateread(self.topic1, self.profile1.user)) + + def test_mark_read(self): + self.assertTrue(self.topic1.never_read(self.profile1.user)) + + # scenario - topic1 : + # post1 - user1 - read + # post2 - user2 - read + mark_read(self.topic1, self.profile1.user) + self.assertFalse(self.topic1.never_read(self.profile1.user)) + + # scenario - topic1 : + # post1 - user1 - read + # post2 - user2 - read + # post3 - user2 - unread + PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=3) + self.assertTrue(self.topic1.never_read(self.profile1.user)) diff --git a/zds/mp/tests/tests_views.py b/zds/mp/tests/tests_views.py new file mode 100644 index 0000000000..540b97725f --- /dev/null +++ b/zds/mp/tests/tests_views.py @@ -0,0 +1,969 @@ +# coding: utf-8 + +import urllib + +from django.test import TestCase +from django.core.urlresolvers import reverse + +from zds.member.factories import ProfileFactory +from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +from zds.mp.models import PrivateTopic, PrivatePost +from zds.utils import slugify + + +class IndexViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + def test_denies_anonymous(self): + response = self.client.get(reverse('zds.mp.views.index'), follow=True) + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.mp.views.index'), '')) + + def test_success_delete_topic_no_participants(self): + topic = PrivateTopicFactory(author=self.profile1.user) + login_check = self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + self.assertEqual(1, PrivateTopic.objects.filter(pk=topic.pk).count()) + + response = self.client.post( + reverse('zds.mp.views.index'), + { + 'delete': '', + 'items': [topic.pk] + } + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(0, PrivateTopic.objects.filter(pk=topic.pk).count()) + + def test_success_delete_topic_as_author(self): + + login_check = self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + response = self.client.post( + reverse('zds.mp.views.index'), + { + 'delete': '', + 'items': [self.topic1.pk] + } + ) + + self.assertEqual(200, response.status_code) + topic = PrivateTopic.objects.get(pk=self.topic1.pk) + self.assertEqual(self.profile2.user, topic.author) + self.assertNotIn(self.profile1.user, topic.participants.all()) + self.assertNotIn(self.profile2.user, topic.participants.all()) + + def test_success_delete_topic_as_participant(self): + + login_check = self.client.login( + username=self.profile2.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + response = self.client.post( + reverse('zds.mp.views.index'), + { + 'delete': '', + 'items': [self.topic1.pk] + } + ) + + self.assertEqual(200, response.status_code) + + topic = PrivateTopic.objects.get(pk=self.topic1.pk) + self.assertNotEqual(self.profile2.user, topic.author) + self.assertNotIn(self.profile1.user, topic.participants.all()) + self.assertNotIn(self.profile2.user, topic.participants.all()) + + def test_fail_delete_topic_not_belong_to_user(self): + topic = PrivateTopicFactory(author=self.profile1.user) + + self.assertEqual(1, PrivateTopic.objects.filter(pk=topic.pk).count()) + + login_check = self.client.login( + username=self.profile2.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + self.client.post( + reverse('zds.mp.views.index'), + { + 'delete': '', + 'items': [topic.pk] + } + ) + + self.assertEqual(1, PrivateTopic.objects.filter(pk=topic.pk).count()) + + +class TopicViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + def test_denies_anonymous(self): + response = self.client.get( + reverse( + 'zds.mp.views.topic', + args=[self.topic1.pk, slugify(self.topic1.title)]), + follow=True) + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse( + 'zds.mp.views.topic', + args=[self.topic1.pk, slugify(self.topic1.title)]), '')) + + def test_fail_topic_no_exist(self): + + login_check = self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + response = self.client.get(reverse( + 'zds.mp.views.topic', + args=[12, 'test'])) + self.assertEqual(404, response.status_code) + + def test_fail_topic_no_permission(self): + topic = PrivateTopicFactory(author=self.profile1.user) + + login_check = self.client.login( + username=self.profile2.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + response = self.client.get(reverse( + 'zds.mp.views.topic', + args=[topic.pk, 'test']), + follow=True + ) + + self.assertEqual(403, response.status_code) + + def test_fail_topic_slug(self): + login_check = self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + response = self.client.get(reverse( + 'zds.mp.views.topic', + args=[self.topic1.pk, 'test']), + follow=True + ) + + self.assertRedirects( + response, + reverse( + 'zds.mp.views.topic', + args=[self.topic1.pk, slugify(self.topic1.title)]), + ) + + +class NewTopicViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + + login_check = self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + def test_denies_anonymous(self): + + self.client.logout() + response = self.client.get(reverse('zds.mp.views.new'), follow=True) + + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.mp.views.new'), '')) + + def test_success_get_with_and_without_username(self): + + response = self.client.get(reverse('zds.mp.views.new')) + + self.assertEqual(200, response.status_code) + self.assertIsNone( + response.context['form'].initial['participants']) + + response2 = self.client.get( + reverse('zds.mp.views.new') + + '?username=' + self.profile2.user.username) + + self.assertEqual(200, response2.status_code) + self.assertEqual( + self.profile2.user.username, + response2.context['form'].initial['participants']) + + def test_fail_get_with_username_not_exist(self): + + response2 = self.client.get( + reverse('zds.mp.views.new') + + '?username=wrongusername') + + self.assertEqual(200, response2.status_code) + self.assertIsNone( + response2.context['form'].initial['participants']) + + def test_success_preview(self): + + self.assertEqual(0, PrivateTopic.objects.all().count()) + response = self.client.post( + reverse('zds.mp.views.new'), + { + 'preview': '', + 'participants': self.profile2.user.username, + 'title': 'title', + 'subtitle': 'subtitle', + 'text': 'text' + } + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(0, PrivateTopic.objects.all().count()) + + def test_fail_new_topic_user_no_exist(self): + + self.assertEqual(0, PrivateTopic.objects.all().count()) + response = self.client.post( + reverse('zds.mp.views.new'), + { + 'participants': 'wronguser', + 'title': 'title', + 'subtitle': 'subtitle', + 'text': 'text' + } + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(0, PrivateTopic.objects.all().count()) + + def test_success_new_topic(self): + + self.assertEqual(0, PrivateTopic.objects.all().count()) + response = self.client.post( + reverse('zds.mp.views.new'), + { + 'participants': self.profile2.user.username, + 'title': 'title', + 'subtitle': 'subtitle', + 'text': 'text' + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(1, PrivateTopic.objects.all().count()) + + def test_fail_new_topic_user_add_only_himself(self): + + self.assertEqual(0, PrivateTopic.objects.all().count()) + response = self.client.post( + reverse('zds.mp.views.new'), + { + 'participants': self.profile1.user.username, + 'title': 'title', + 'subtitle': 'subtitle', + 'text': 'text' + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(0, PrivateTopic.objects.all().count()) + + def test_fail_new_topic_user_add_himself_and_others(self): + + self.assertEqual(0, PrivateTopic.objects.all().count()) + + participants = self.profile2.user.username + + response = self.client.post( + reverse('zds.mp.views.new'), + { + 'participants': participants, + 'title': 'title', + 'subtitle': 'subtitle', + 'text': 'text' + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(1, PrivateTopic.objects.all().count()) + self.assertNotIn( + self.profile1.user, + PrivateTopic.objects.all()[0].participants.all() + ) + + +class EditViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.profile3 = ProfileFactory() + + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + login_check = self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + self.assertTrue(login_check) + + def test_denies_anonymous(self): + + self.client.logout() + response = self.client.get(reverse('zds.mp.views.edit'), follow=True) + + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.mp.views.edit'), '')) + + def test_fail_edit_topic_not_sending_topic_pk(self): + + response = self.client.post(reverse('zds.mp.views.edit')) + + self.assertEqual(404, response.status_code) + + def test_fail_edit_topic_no_exist(self): + + response = self.client.post( + reverse('zds.mp.views.edit'), + { + 'privatetopic': 156 + } + ) + + self.assertEqual(404, response.status_code) + + def test_fail_edit_topic_add_no_exist_user(self): + + response = self.client.post( + reverse('zds.mp.views.edit'), + { + 'privatetopic': self.topic1.pk, + 'username': 'wrongusername' + } + ) + + self.assertEqual(404, response.status_code) + + def test_success_edit_topic_add_participant(self): + + response = self.client.post( + reverse('zds.mp.views.edit'), + { + 'privatetopic': self.topic1.pk, + 'username': self.profile3.user.username + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + topic = PrivateTopic.objects.get(pk=self.topic1.pk) + self.assertIn( + self.profile3.user, + topic.participants.all() + ) + + def test_fail_user_add_himself_to_private_topic_with_no_right(self): + + self.client.logout() + self.assertTrue( + self.client.login( + username=self.profile3.user.username, + password='hostel77' + ) + ) + + response = self.client.post( + reverse('zds.mp.views.edit'), + { + 'privatetopic': self.topic1.pk, + 'username': self.profile3.user.username + }, + follow=True + ) + + self.assertEqual(403, response.status_code) + topic = PrivateTopic.objects.get(pk=self.topic1.pk) + self.assertNotIn( + self.profile3.user, + topic.participants.all() + ) + + +class AnswerViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + self.profile3 = ProfileFactory() + + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + self.assertTrue( + self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + ) + + def test_denies_anonymous(self): + + self.client.logout() + response = self.client.get(reverse('zds.mp.views.answer'), follow=True) + + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.mp.views.answer'), '')) + + def test_fail_answer_not_send_topic_pk(self): + + response = self.client.post( + reverse('zds.mp.views.answer'), + {} + ) + + self.assertEqual(404, response.status_code) + + def test_fail_answer_topic_no_exist(self): + + response = self.client.post( + reverse('zds.mp.views.answer') + '?sujet=156', + {} + ) + + self.assertEqual(404, response.status_code) + + def test_fail_cite_post_no_exist(self): + + response = self.client.get( + reverse('zds.mp.views.answer') + + '?sujet=' + str(self.topic1.pk) + + '&cite=4864', + {} + ) + + self.assertEqual(404, response.status_code) + + def test_success_cite_post(self): + + response = self.client.get( + reverse('zds.mp.views.answer') + + '?sujet=' + str(self.topic1.pk) + + '&cite=' + str(self.post1.pk), + {} + ) + + self.assertEqual(200, response.status_code) + + def test_success_preview_answer(self): + + response = self.client.post( + reverse('zds.mp.views.answer') + + '?sujet=' + str(self.topic1.pk), + { + 'text': 'answer', + 'preview': '', + 'last_post': self.topic1.get_last_answer().pk + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + + def test_success_answer(self): + + response = self.client.post( + reverse('zds.mp.views.answer') + + '?sujet=' + str(self.topic1.pk), + { + 'text': 'answer', + 'last_post': self.topic1.get_last_answer().pk + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(3, PrivatePost.objects.all().count()) + + def test_fail_answer_with_no_right(self): + + self.client.logout() + self.assertTrue( + self.client.login( + username=self.profile3.user.username, + password='hostel77' + ) + ) + + response = self.client.post( + reverse('zds.mp.views.answer') + + '?sujet=' + str(self.topic1.pk), + { + 'text': 'answer', + 'last_post': self.topic1.get_last_answer().pk + }, + follow=True + ) + + self.assertEqual(403, response.status_code) + self.assertEqual(2, PrivatePost.objects.all().count()) + + +class EditPostViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + self.assertTrue( + self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + ) + + def test_denies_anonymous(self): + + self.client.logout() + response = self.client.get( + reverse('zds.mp.views.edit_post'), + follow=True + ) + + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.mp.views.edit_post'), '')) + + def test_fail_edit_post_no_get_parameter(self): + + response = self.client.get( + reverse('zds.mp.views.edit_post') + ) + + self.assertEqual(404, response.status_code) + + def test_succes_get_edit_post_page(self): + + self.client.logout() + self.assertTrue( + self.client.login( + username=self.profile2.user.username, + password='hostel77' + ) + ) + + response = self.client.get( + reverse('zds.mp.views.edit_post') + + '?message=' + str(self.post2.pk) + ) + + self.assertEqual(200, response.status_code) + + def test_fail_edit_post_no_exist(self): + + response = self.client.get( + reverse('zds.mp.views.edit_post') + + '?message=154' + ) + + self.assertEqual(404, response.status_code) + + def test_fail_edit_post_not_last(self): + + response = self.client.get( + reverse('zds.mp.views.edit_post') + + '?message=' + str(self.post1.pk) + ) + + self.assertEqual(403, response.status_code) + + def test_fail_edit_post_with_no_right(self): + + response = self.client.get( + reverse('zds.mp.views.edit_post') + + '?message=' + str(self.post2.pk) + ) + + self.assertEqual(403, response.status_code) + + def test_success_edit_post_preview(self): + + self.client.logout() + self.assertTrue( + self.client.login( + username=self.profile2.user.username, + password='hostel77' + ) + ) + + response = self.client.post( + reverse('zds.mp.views.edit_post') + + '?message=' + str(self.post2.pk), + { + 'text': 'update post', + 'preview': '' + } + ) + + self.assertEqual(200, response.status_code) + self.assertEqual( + 'update post', + response.context['form'].initial['text'] + ) + + def test_success_edit_post(self): + + self.client.logout() + self.assertTrue( + self.client.login( + username=self.profile2.user.username, + password='hostel77' + ) + ) + + response = self.client.post( + reverse('zds.mp.views.edit_post') + + '?message=' + str(self.post2.pk), + { + 'text': 'update post', + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual( + 'update post', + PrivatePost.objects.get(pk=self.post2.pk).text + ) + + +class LeaveViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + self.assertTrue( + self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + ) + + def test_denies_anonymous(self): + + self.client.logout() + response = self.client.get(reverse('zds.mp.views.leave'), follow=True) + + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.mp.views.leave'), '')) + + def test_fail_leave_topic_no_exist(self): + + response = self.client.post( + reverse('zds.mp.views.leave'), + { + 'leave': '', + 'topic_pk': '154' + } + ) + + self.assertEqual(404, response.status_code) + + def test_success_leave_topic_as_author_no_participants(self): + + self.topic1.participants.remove(self.profile2) + self.topic1.save() + + response = self.client.post( + reverse('zds.mp.views.leave'), + { + 'leave': '', + 'topic_pk': self.topic1.pk + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual( + 0, + PrivateTopic.objects.all().count() + ) + + def test_success_leave_topic_as_author(self): + + response = self.client.post( + reverse('zds.mp.views.leave'), + { + 'leave': '', + 'topic_pk': self.topic1.pk + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual( + 1, + PrivateTopic.objects.all().count() + ) + + self.assertEqual( + self.profile2.user, + PrivateTopic.objects.get(pk=self.topic1.pk).author + ) + + def test_success_leave_topic_as_participant(self): + + self.client.logout() + self.assertTrue( + self.client.login( + username=self.profile2.user.username, + password='hostel77' + ) + ) + + response = self.client.post( + reverse('zds.mp.views.leave'), + { + 'leave': '', + 'topic_pk': self.topic1.pk + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + + self.assertNotIn( + self.profile2.user, + PrivateTopic.objects.get(pk=self.topic1.pk).participants.all() + ) + + self.assertNotEqual( + self.profile2.user, + PrivateTopic.objects.get(pk=self.topic1.pk).author + ) + + +class AddParticipantViewTest(TestCase): + + def setUp(self): + self.profile1 = ProfileFactory() + self.profile2 = ProfileFactory() + + self.topic1 = PrivateTopicFactory(author=self.profile1.user) + self.topic1.participants.add(self.profile2.user) + self.post1 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile1.user, + position_in_topic=1) + + self.post2 = PrivatePostFactory( + privatetopic=self.topic1, + author=self.profile2.user, + position_in_topic=2) + + self.assertTrue( + self.client.login( + username=self.profile1.user.username, + password='hostel77' + ) + ) + + def test_denies_anonymous(self): + + self.client.logout() + response = self.client.get( + reverse('zds.mp.views.add_participant'), + follow=True + ) + + self.assertRedirects( + response, + reverse('zds.member.views.login_view') + + '?next=' + urllib.quote(reverse('zds.mp.views.add_participant'), '')) + + def test_fail_add_participant_topic_no_exist(self): + + response = self.client.post( + reverse('zds.mp.views.add_participant'), + { + 'topic_pk': '451' + }, + follow=True + ) + + self.assertEqual(404, response.status_code) + + def test_fail_add_participant_who_no_exist(self): + + response = self.client.post( + reverse('zds.mp.views.add_participant'), + { + 'topic_pk': self.topic1.pk, + 'user_pk': '178548' + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(1, len(response.context['messages'])) + + def test_fail_add_participant_with_no_right(self): + profile3 = ProfileFactory() + + self.client.logout() + self.assertTrue( + self.client.login( + username=profile3.user.username, + password='hostel77' + ) + ) + + response = self.client.post( + reverse('zds.mp.views.add_participant'), + { + 'topic_pk': self.topic1.pk, + 'user_pk': profile3.user.username + } + ) + + self.assertEqual(403, response.status_code) + self.assertNotIn( + profile3.user, + PrivateTopic.objects.get(pk=self.topic1.pk).participants.all() + ) + + def test_fail_add_participant_already_in(self): + + response = self.client.post( + reverse('zds.mp.views.add_participant'), + { + 'topic_pk': self.topic1.pk, + 'user_pk': self.profile2.user.username + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(1, len(response.context['messages'])) + + def test_success_add_participant(self): + + profile3 = ProfileFactory() + + response = self.client.post( + reverse('zds.mp.views.add_participant'), + { + 'topic_pk': self.topic1.pk, + 'user_pk': profile3.user.username + }, + follow=True + ) + + self.assertEqual(200, response.status_code) + self.assertEqual(1, len(response.context['messages'])) + self.assertIn( + profile3.user, + PrivateTopic.objects.get(pk=self.topic1.pk).participants.all() + ) diff --git a/zds/mp/views.py b/zds/mp/views.py index 83f2376c9a..42a523a52c 100644 --- a/zds/mp/views.py +++ b/zds/mp/views.py @@ -17,6 +17,7 @@ from django.template import Context from django.template.loader import get_template from django.views.decorators.http import require_POST +from django.forms.util import ErrorList from zds.utils import render_template, slugify from zds.utils.mps import send_mp @@ -26,8 +27,6 @@ from .forms import PrivateTopicForm, PrivatePostForm from .models import PrivateTopic, PrivatePost, \ never_privateread, mark_read, PrivateTopicRead -from django.db.models.query_utils import select_related_descend - @login_required @@ -38,7 +37,11 @@ def index(request): if request.method == 'POST': if 'delete' in request.POST: liste = request.POST.getlist('items') - topics = PrivateTopic.objects.filter(pk__in=liste).all() + topics = PrivateTopic.objects.filter(pk__in=liste)\ + .filter( + Q(participants__in=[request.user]) + | Q(author=request.user)) + for topic in topics: if topic.participants.all().count() == 0: topic.delete() @@ -75,7 +78,6 @@ def index(request): }) - @login_required def topic(request, topic_pk, topic_slug): """Display a thread and its posts using a pager.""" @@ -139,7 +141,6 @@ def topic(request, topic_pk, topic_slug): }) - @login_required def new(request): """Creates a new private topic.""" @@ -147,13 +148,13 @@ def new(request): if request.method == 'POST': # If the client is using the "preview" button if 'preview' in request.POST: - form = PrivateTopicForm(request.user.username, - initial={ - 'participants': request.POST['participants'], - 'title': request.POST['title'], - 'subtitle': request.POST['subtitle'], - 'text': request.POST['text'], - }) + form = PrivateTopicForm(request.user.username, + initial={ + 'participants': request.POST['participants'], + 'title': request.POST['title'], + 'subtitle': request.POST['subtitle'], + 'text': request.POST['text'], + }) return render_template('mp/topic/new.html', { 'form': form, }) @@ -176,6 +177,16 @@ def new(request): continue ctrl.append(p) + # user add only himself + if (len(ctrl) < 1 + and len(list_part) == 1 + and list_part[0] == request.user.username): + errors = form._errors.setdefault("participants", ErrorList()) + errors.append(u'Vous êtes déjà auteur du message') + return render_template('mp/topic/new.html', { + 'form': form, + }) + p_topic = send_mp(request.user, ctrl, data['title'], @@ -200,7 +211,8 @@ def new(request): destList.append(User.objects.get(username=username).username) except: pass - dest = ', '.join(destList) + if len(destList) > 0: + dest = ', '.join(destList) form = PrivateTopicForm(username=request.user.username, initial={ @@ -211,12 +223,10 @@ def new(request): }) - @login_required @require_POST def edit(request): """Edit the given topic.""" - authenticated_user = request.user try: topic_pk = request.POST['privatetopic'] @@ -232,14 +242,13 @@ def edit(request): if request.POST['username']: u = get_object_or_404(User, username=request.POST['username']) - if not authenticated_user == u: + if not request.user == u: g_topic.participants.add(u) g_topic.save() return redirect(u'{}?page={}'.format(g_topic.get_absolute_url(), page)) - @login_required def answer(request): """Adds an answer from an user to a topic.""" @@ -251,11 +260,16 @@ def answer(request): # Retrieve current topic. g_topic = get_object_or_404(PrivateTopic, pk=topic_pk) - # Retrieve 3 last posts of the currenta topic. - posts = PrivatePost.objects\ - .filter(privatetopic=g_topic)\ - .order_by('-pubdate')[:3] + # check if user has right to answer + if not g_topic.author == request.user \ + and request.user not in list(g_topic.participants.all()): + raise PermissionDenied + last_post_pk = g_topic.last_message.pk + # Retrieve last posts of the current private topic. + posts = PrivatePost.objects.filter(privatetopic=g_topic) \ + .prefetch_related() \ + .order_by("-pubdate")[:settings.POSTS_PER_PAGE] # User would like preview his post or post a new post on the topic. if request.method == 'POST': @@ -270,6 +284,7 @@ def answer(request): return render_template('mp/post/new.html', { 'topic': g_topic, 'last_post_pk': last_post_pk, + 'posts': posts, 'newpost': newpost, 'form': form, }) @@ -337,6 +352,7 @@ def answer(request): 'topic': g_topic, 'last_post_pk': last_post_pk, 'newpost': newpost, + 'posts': posts, 'form': form, }) @@ -346,7 +362,7 @@ def answer(request): # Using the quote button if 'cite' in request.GET: post_cite_pk = request.GET['cite'] - post_cite = PrivatePost.objects.get(pk=post_cite_pk) + post_cite = get_object_or_404(PrivatePost, pk=post_cite_pk) for line in post_cite.text.splitlines(): text = text + '> ' + line + '\n' @@ -368,7 +384,6 @@ def answer(request): }) - @login_required def edit_post(request): """Edit the given user's post.""" @@ -442,13 +457,12 @@ def edit_post(request): }) - @login_required @require_POST @transaction.atomic def leave(request): if 'leave' in request.POST: - ptopic = PrivateTopic.objects.get(pk=request.POST['topic_pk']) + ptopic = get_object_or_404(PrivateTopic, pk=request.POST['topic_pk']) if ptopic.participants.count() == 0: ptopic.delete() elif request.user.pk == ptopic.author.pk: @@ -466,14 +480,19 @@ def leave(request): return redirect(reverse('zds.mp.views.index')) - @login_required @require_POST @transaction.atomic def add_participant(request): - ptopic = PrivateTopic.objects.get(pk=request.POST['topic_pk']) + ptopic = get_object_or_404(PrivateTopic, pk=request.POST['topic_pk']) + + # check if user is the author of topic + if not ptopic.author == request.user: + raise PermissionDenied + try: - part = User.objects.get(username=request.POST['user_pk']) + # user_pk or user_username ? + part = User.objects.get(username__exact=request.POST['user_pk']) if part.pk == ptopic.author.pk or part in ptopic.participants.all(): messages.warning( request, diff --git a/zds/munin/views.py b/zds/munin/views.py index 51ad27e2bb..ea096cf98f 100644 --- a/zds/munin/views.py +++ b/zds/munin/views.py @@ -35,7 +35,6 @@ def total_tutorials(request): ("online", tutorials.filter(sha_public__isnull=False).count())] - @muninview(config="""graph_title Total articles graph_vlabel articles""") def total_articles(request): diff --git a/zds/pages/forms.py b/zds/pages/forms.py index b7df380f1d..31fb2792bb 100644 --- a/zds/pages/forms.py +++ b/zds/pages/forms.py @@ -7,56 +7,41 @@ class AssocSubscribeForm(forms.Form): - first_name = forms.CharField( - label=u'Prénom', - max_length=30, + full_name = forms.CharField( + label=u'Qui êtes vous ?', + max_length=50, required=True, + widget=forms.TextInput( + attrs={ + 'placeholder': u'M/Mme Prénom Nom' + } + ) ) - surname = forms.CharField( - label='Nom de famille', - max_length=30, + email = forms.EmailField( + label=u'Adresse courriel', required=True, ) - email = forms.EmailField( - label='Adresse courriel', + naissance = forms.CharField( + label=u'Date de naissance', required=True, ) adresse = forms.CharField( label=u'Adresse', - max_length=38, - required=True, - ) - - adresse_complement = forms.CharField( - label=u'Complément d\'adresse', - max_length=38, - required=False, - ) - - code_postal = forms.CharField( - label='Code Postal', - max_length=10, - required=True, - ) - - ville = forms.CharField( - label='Ville', - max_length=38, - required=True, - ) - - pays = forms.CharField( - label='Pays', - max_length=38, required=True, + widget=forms.Textarea( + attrs={ + 'placeholder': u'Votre adresse complète (rue, code postal, ville, pays...)' + } + ) ) justification = forms.CharField( - label='Pourquoi voulez-vous adhérer à l\'association ?', + label=u'Pourquoi voulez-vous adhérer à l\'association ?', required=False, + max_length=3000, widget=forms.Textarea( attrs={ 'placeholder': u'Décrivez ici la raison de votre demande d\'adhésion à l\'association.' @@ -71,14 +56,10 @@ def __init__(self, *args, **kwargs): self.helper.form_method = 'post' self.helper.layout = Layout( - Field('first_name'), - Field('surname'), + Field('full_name'), Field('email'), + Field('naissance'), Field('adresse'), - Field('adresse_complement'), - Field('code_postal'), - Field('ville'), - Field('pays'), Field('justification'), ButtonHolder( StrictButton('Valider', type='submit'), @@ -88,7 +69,7 @@ def __init__(self, *args, **kwargs): def clean(self): cleaned_data = super(AssocSubscribeForm, self).clean() justification = cleaned_data.get('justification') - + if justification is not None and len(justification) > 3000: self._errors['justification'] = self.error_class( - [(u'Ce message est trop long, il ne doit pas dépasser 3000 caractères')]) \ No newline at end of file + [(u'Ce message est trop long, il ne doit pas dépasser 3000 caractères')]) diff --git a/zds/pages/tests.py b/zds/pages/tests.py index 7a584cb141..ca0039de63 100644 --- a/zds/pages/tests.py +++ b/zds/pages/tests.py @@ -74,14 +74,10 @@ def test_subscribe_association(self): result = self.client.post( reverse('zds.pages.views.assoc_subscribe'), { - 'first_name': 'Anne', - 'surname': 'Onyme', + 'full_name': 'Anne Onyme', 'email': 'anneonyme@test.com', - 'adresse': '42 rue du savoir', - 'adresse_complement': 'appartement 42', - 'code_postal': '75000', - 'ville': 'Paris', - 'pays': 'France', + 'naissance': '01 janvier 1970', + 'adresse': '42 rue du savoir, appartement 42, 75000 Paris, France', 'justification': long_str, 'username': self.user1.username, 'profile_url': settings.SITE_URL + reverse('zds.member.views.details', @@ -97,14 +93,10 @@ def test_subscribe_association(self): result = self.client.post( reverse('zds.pages.views.assoc_subscribe'), { - 'first_name': 'Anne', - 'surname': 'Onyme', + 'full_name': 'Anne Onyme', 'email': 'anneonyme@test.com', - 'adresse': '42 rue du savoir', - 'adresse_complement': 'appartement 42', - 'code_postal': '75000', - 'ville': 'Paris', - 'pays': 'France', + 'naissance': '01 janvier 1970', + 'adresse': '42 rue du savoir, appartement 42, 75000 Paris, France', 'justification': 'Parce que l\'assoc est trop swag !', 'username': self.user1.username, 'profile_url': settings.SITE_URL + reverse('zds.member.views.details', diff --git a/zds/pages/views.py b/zds/pages/views.py index 4c920db3ef..016f91205b 100644 --- a/zds/pages/views.py +++ b/zds/pages/views.py @@ -14,12 +14,11 @@ from zds import settings from zds.article.models import get_last_articles -from zds.forum.models import get_last_topics from zds.member.decorator import can_write_and_read_now from zds.pages.forms import AssocSubscribeForm from zds.settings import SITE_ROOT from zds.tutorial.models import get_last_tutorials -from zds.utils import render_template, slugify +from zds.utils import render_template from zds.utils.models import Alert @@ -31,7 +30,7 @@ def home(request): data = tuto.load_json_for_public() tuto.load_dic(data) tutos.append(data) - + articles = [] for article in get_last_articles(): data = article.load_json_for_public() @@ -69,14 +68,10 @@ def assoc_subscribe(request): user = request.user data = form.data context = { - 'first_name': data['first_name'], - 'surname': data['surname'], + 'full_name': data['full_name'], 'email': data['email'], + 'naissance': data['naissance'], 'adresse': data['adresse'], - 'adresse_complement': data['adresse_complement'], - 'code_postal': data['code_postal'], - 'ville': data['ville'], - 'pays': data['pays'], 'justification': data['justification'], 'username': user.username, 'profile_url': settings.SITE_URL + reverse('zds.member.views.details', @@ -96,10 +91,13 @@ def assoc_subscribe(request): msg.attach_alternative(message_html, "text/html") try: msg.send() - messages.success(request, "Votre demande d'adhésion a bien été envoyée et va être étudiée.") + messages.success(request, u"Votre demande d'adhésion a bien été envoyée et va être étudiée.") except: msg = None messages.error(request, "Une erreur est survenue.") + + # reset the form after successfull validation + form = AssocSubscribeForm() return render_template("pages/assoc_subscribe.html", {"form": form}) form = AssocSubscribeForm(initial={'email': request.user.email}) diff --git a/zds/search/constants.py b/zds/search/constants.py index 66d75238b4..2812117357 100644 --- a/zds/search/constants.py +++ b/zds/search/constants.py @@ -13,4 +13,4 @@ 'post': ['Message du forum', 'Messages du forum'], 'topic': ['Sujet du forum', 'Sujets du forum'] } -} \ No newline at end of file +} diff --git a/zds/search/forms.py b/zds/search/forms.py index 2245661a04..cd52a651be 100644 --- a/zds/search/forms.py +++ b/zds/search/forms.py @@ -8,22 +8,30 @@ class CustomSearchForm(ModelSearchForm): + def __init__(self, *args, **kwargs): super(CustomSearchForm, self).__init__(*args, **kwargs) self.fields['models'] = forms.MultipleChoiceField( - choices=self.model_choices(), - required=False, - label='Rechercher dans', - widget=forms.CheckboxSelectMultiple( - attrs={ - 'form': 'search_form' - } - ) - ) + choices=self.model_choices(), + required=False, + label='Rechercher dans', + widget=forms.CheckboxSelectMultiple( + attrs={ + 'form': 'search_form' + } + ) + ) def model_choices(self, using=DEFAULT_ALIAS): - choices = [("%s.%s" % (m._meta.app_label, m._meta.module_name), self.get_model_name(m._meta.app_label, m._meta.module_name, True)) for m in connections[using].get_unified_index().get_indexed_models()] + choices = [ + ("%s.%s" % + (m._meta.app_label, + m._meta.module_name), + self.get_model_name( + m._meta.app_label, + m._meta.module_name, + True)) for m in connections[using].get_unified_index().get_indexed_models()] return sorted(choices, key=lambda x: x[1]) def get_model_name(self, app_label, module_name, plural): - return MODEL_NAMES[app_label][module_name][plural]; \ No newline at end of file + return MODEL_NAMES[app_label][module_name][plural] diff --git a/zds/search/views.py b/zds/search/views.py index 489bc8fd8f..efb28c5693 100644 --- a/zds/search/views.py +++ b/zds/search/views.py @@ -5,7 +5,9 @@ from zds.utils.paginator import paginator_range from zds.utils import render_template + class CustomSearchView(SearchView): + def create_response(self): (paginator, page) = self.build_page() diff --git a/zds/settings.py b/zds/settings.py index fba44ab6a0..64083a867c 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -151,7 +151,7 @@ 'django.contrib.staticfiles', 'django.contrib.sitemaps', 'django.contrib.humanize', - + 'easy_thumbnails', 'easy_thumbnails.optimize', 'south', @@ -248,11 +248,19 @@ # Max size image upload (in bytes) IMAGE_MAX_SIZE = 1024 * 1024 +# Constant for anonymisation + +ANONYMOUS_USER = "anonymous" +EXTERNAL_USER = "Auteur externe" + # git directory REPO_PATH = os.path.join(SITE_ROOT, 'tutoriels-private') REPO_PATH_PROD = os.path.join(SITE_ROOT, 'tutoriels-public') REPO_ARTICLE_PATH = os.path.join(SITE_ROOT, 'articles-data') +# Constant for tags +TOP_TAG_MAX = 2 + # Constants for pagination POSTS_PER_PAGE = 21 TOPICS_PER_PAGE = 21 @@ -263,8 +271,12 @@ SPAM_LIMIT_PARTICIPANT = 2 FOLLOWED_TOPICS_PER_PAGE = 21 +# username of the bot who send MP BOT_ACCOUNT = 'admin' +# primary key of beta forum +BETA_FORUM_ID = 1 + PANDOC_LOC = '' # LOG PATH FOR PANDOC LOGGING PANDOC_LOG = './pandoc.log' diff --git a/zds/tutorial/factories.py b/zds/tutorial/factories.py index ab44cbd52b..b680fbd63d 100644 --- a/zds/tutorial/factories.py +++ b/zds/tutorial/factories.py @@ -2,14 +2,6 @@ from datetime import datetime from git.repo import Repo -try: - import ujson as json_reader -except: - try: - import simplejson as json_reader - except: - import json as json_reader - import json as json_writer import os @@ -17,22 +9,27 @@ from zds.tutorial.models import Tutorial, Part, Chapter, Extract, Note,\ Validation -from zds.utils.models import SubCategory, Licence +from zds.utils.models import SubCategory, Licence +from zds.gallery.factories import GalleryFactory, UserGalleryFactory from zds.utils.tutorials import export_tutorial -contenu = ( -u'Ceci est un contenu de tutoriel utile et à tester un peu partout' -u'Ce contenu ira aussi bien dans les introductions, que dans les conclusions et les extraits ' -u'le gros intéret étant qu\'il renferme des images pour tester l\'execution coté pandoc ' -u'Exemple d\'image ![Ma pepite souris](http://blog.science-infuse.fr/public/souris.jpg)' -u'\nExemple d\'image ![Image inexistante](http://blog.science-infuse.fr/public/inv_souris.jpg)' -u'\nExemple de gif ![](http://corigif.free.fr/oiseau/img/oiseau_004.gif)' -u'\nExemple de gif inexistant ![](http://corigif.free.fr/oiseau/img/ironman.gif)' -u'Une image de type wikipedia qui fait tomber des tests ![](https://s.qwant.com/thumbr/?u=http%3A%2F%2Fwww.blogoergosum.com%2Fwp-content%2Fuploads%2F2010%2F02%2Fwikipedia-logo.jpg&h=338&w=600)' -u'Image dont le serveur n\'existe pas ![](http://unknown.image.zds)' -u'\n Attention les tests ne doivent pas crasher ' -u'qu\'un sujet abandonné !') - +content = ( + u'Ceci est un contenu de tutoriel utile et à tester un peu partout' + u'Ce contenu ira aussi bien dans les introductions, que dans les conclusions et les extraits ' + u'le gros intéret étant qu\'il renferme des images pour tester l\'execution coté pandoc ' + u'Exemple d\'image ![Ma pepite souris](http://blog.science-infuse.fr/public/souris.jpg)' + u'\nExemple d\'image ![Image inexistante](http://blog.science-infuse.fr/public/inv_souris.jpg)' + u'\nExemple de gif ![](http://corigif.free.fr/oiseau/img/oiseau_004.gif)' + u'\nExemple de gif inexistant ![](http://corigif.free.fr/oiseau/img/ironman.gif)' + u'Une image de type wikipedia qui fait tomber des tests ![](https://s.qwant.com/thumbr/?u=http%3A%2' + u'F%2Fwww.blogoergosum.com%2Fwp-content%2Fuploads%2F2010%2F02%2Fwikipedia-logo.jpg&h=338&w=600)' + u'Image dont le serveur n\'existe pas ![](http://unknown.image.zds)' + u'\n Attention les tests ne doivent pas crasher ' + u'qu\'un sujet abandonné !') + +content_light = u'Un contenu light pour quand ce n\'est pas vraiment ça qui est testé' + + class BigTutorialFactory(factory.DjangoModelFactory): FACTORY_FOR = Tutorial @@ -46,8 +43,13 @@ class BigTutorialFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): + + light = kwargs.pop('light', False) tuto = super(BigTutorialFactory, cls)._prepare(create, **kwargs) path = tuto.get_path() + real_content = content + if light: + real_content = content_light if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -59,16 +61,19 @@ def _prepare(cls, create, **kwargs): f.write(json_writer.dumps(man, indent=4, ensure_ascii=False).encode('utf-8')) f.close() f = open(os.path.join(path, tuto.introduction), "w") - f.write(contenu.encode('utf-8')) + f.write(real_content.encode('utf-8')) f.close() f = open(os.path.join(path, tuto.conclusion), "w") - f.write(contenu.encode('utf-8')) + f.write(real_content.encode('utf-8')) f.close() repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) cm = repo.index.commit("Init Tuto") tuto.sha_draft = cm.hexsha tuto.sha_beta = None + tuto.gallery = GalleryFactory() + for author in tuto.authors.all(): + UserGalleryFactory(user=author, gallery=tuto.gallery) return tuto @@ -85,7 +90,12 @@ class MiniTutorialFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): + light = kwargs.pop('light', False) tuto = super(MiniTutorialFactory, cls)._prepare(create, **kwargs) + real_content = content + + if light: + real_content = content_light path = tuto.get_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -102,16 +112,19 @@ def _prepare(cls, create, **kwargs): ensure_ascii=False).encode('utf-8')) file.close() file = open(os.path.join(path, tuto.introduction), "w") - file.write(contenu.encode('utf-8')) + file.write(real_content.encode('utf-8')) file.close() file = open(os.path.join(path, tuto.conclusion), "w") - file.write(contenu.encode('utf-8')) + file.write(real_content.encode('utf-8')) file.close() repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) cm = repo.index.commit("Init Tuto") tuto.sha_draft = cm.hexsha + tuto.gallery = GalleryFactory() + for author in tuto.authors.all(): + UserGalleryFactory(user=author, gallery=tuto.gallery) return tuto @@ -122,9 +135,14 @@ class PartFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): + light = kwargs.pop('light', False) part = super(PartFactory, cls)._prepare(create, **kwargs) tutorial = kwargs.pop('tutorial', None) + real_content = content + if light: + real_content = content_light + path = part.get_path() repo = Repo(part.tutorial.get_path()) @@ -136,11 +154,11 @@ def _prepare(cls, create, **kwargs): part.save() f = open(os.path.join(tutorial.get_path(), part.introduction), "w") - f.write(contenu.encode('utf-8')) + f.write(real_content.encode('utf-8')) f.close() repo.index.add([part.introduction]) f = open(os.path.join(tutorial.get_path(), part.conclusion), "w") - f.write(contenu.encode('utf-8')) + f.write(real_content.encode('utf-8')) f.close() repo.index.add([part.conclusion]) @@ -174,10 +192,15 @@ class ChapterFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): + + light = kwargs.pop('light', False) chapter = super(ChapterFactory, cls)._prepare(create, **kwargs) tutorial = kwargs.pop('tutorial', None) part = kwargs.pop('part', None) + real_content = content + if light: + real_content = content_light path = chapter.get_path() if not os.path.isdir(path): @@ -214,14 +237,14 @@ def _prepare(cls, create, **kwargs): part.tutorial.get_path(), chapter.introduction), "w") - f.write(contenu.encode('utf-8')) + f.write(real_content.encode('utf-8')) f.close() f = open( os.path.join( part.tutorial.get_path(), chapter.conclusion), "w") - f.write(contenu.encode('utf-8')) + f.write(real_content.encode('utf-8')) f.close() part.tutorial.save() repo = Repo(part.tutorial.get_path()) @@ -292,6 +315,7 @@ def _prepare(cls, create, **kwargs): tutorial.save() return note + class SubCategoryFactory(factory.DjangoModelFactory): FACTORY_FOR = SubCategory @@ -302,10 +326,11 @@ class SubCategoryFactory(factory.DjangoModelFactory): class VaidationFactory(factory.DjangoModelFactory): FACTORY_FOR = Validation - + + class LicenceFactory(factory.DjangoModelFactory): FACTORY_FOR = Licence - + code = u'Licence bidon' title = u'Licence bidon' diff --git a/zds/tutorial/feeds.py b/zds/tutorial/feeds.py new file mode 100644 index 0000000000..560953e2d8 --- /dev/null +++ b/zds/tutorial/feeds.py @@ -0,0 +1,43 @@ +# coding: utf-8 + +from django.contrib.syndication.views import Feed + +from django.utils.feedgenerator import Atom1Feed + +from .models import Tutorial + + +class LastTutorialsFeedRSS(Feed): + title = "Tutoriels sur Zeste de Savoir" + link = "/tutoriels/" + description = "Les derniers tutoriels parus sur Zeste de Savoir." + + def items(self): + return Tutorial.objects\ + .filter(sha_public__isnull=False)\ + .order_by('-pubdate')[:5] + + def item_title(self, item): + return item.title + + def item_pubdate(self, item): + return item.pubdate + + def item_description(self, item): + return item.description + + def item_author_name(self, item): + authors_list = item.authors.all() + authors = [] + for authors_obj in authors_list: + authors.append(authors_obj.username) + authors = ", ".join(authors) + return authors + + def item_link(self, item): + return item.get_absolute_url() + + +class LastTutorialsFeedATOM(LastTutorialsFeedRSS): + feed_type = Atom1Feed + subtitle = LastTutorialsFeedRSS.description diff --git a/zds/tutorial/forms.py b/zds/tutorial/forms.py index 48b49e05a6..e5358f39c6 100644 --- a/zds/tutorial/forms.py +++ b/zds/tutorial/forms.py @@ -294,10 +294,38 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( Field('file'), Field('images'), - Submit('submit', 'Importer'), + Submit('import-tuto', 'Importer le .tuto'), ) super(ImportForm, self).__init__(*args, **kwargs) + +class ImportArchiveForm(forms.Form): + + file = forms.FileField( + label='Sélectionnez l\'archive de votre tutoriel', + required=True + ) + + tutorial = forms.ModelChoiceField( + label="Tutoriel vers lequel vous souhaitez importer votre archive", + queryset=Tutorial.objects.none(), + required=True + ) + + def __init__(self, user, *args, **kwargs): + super(ImportArchiveForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + self.fields['tutorial'].queryset = Tutorial.objects.filter(authors__in=[user]) + + self.helper.layout = Layout( + Field('file'), + Field('tutorial'), + Submit('import-archive', 'Importer l\'archive'), + ) + + # Notes @@ -388,14 +416,13 @@ def __init__(self, *args, **kwargs): self.helper.form_method = 'post' self.helper.layout = Layout( - CommonLayoutModalText(), + CommonLayoutModalText(), Field('source'), StrictButton( 'Confirmer', type='submit'), - Hidden( - 'tutorial', '{{ tutorial.pk }}'), Hidden( - 'version', '{{ version }}'), ) + Hidden('tutorial', '{{ tutorial.pk }}'), + Hidden('version', '{{ version }}'), ) class ValidForm(forms.Form): diff --git a/zds/tutorial/models.py b/zds/tutorial/models.py index 742ff802f8..56e4f5dbc5 100644 --- a/zds/tutorial/models.py +++ b/zds/tutorial/models.py @@ -1,6 +1,7 @@ # coding: utf-8 from math import ceil +import shutil try: import ujson as json_reader except: @@ -154,15 +155,13 @@ def in_beta(self): return (self.sha_beta is not None) and (self.sha_beta.strip() != '') def in_validation(self): - return ( - self.sha_validation is not None) and ( - self.sha_validation.strip() != '') + return (self.sha_validation is not None) and (self.sha_validation.strip() != '') def in_drafting(self): return (self.sha_draft is not None) and (self.sha_draft.strip() != '') def on_line(self): - return (self.sha_public is not None) and (self.sha_public.strip() != '') + return (self.sha_public is not None) and (self.sha_public.strip() != '') def is_mini(self): return self.type == 'MINI' @@ -186,20 +185,20 @@ def load_dic(self, mandata, sha=None): '''fill mandata with informations from database model''' fns = [ - 'is_big', 'is_mini', 'have_markdown','have_html', 'have_pdf', + 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'get_path', 'in_beta', 'in_validation', 'on_line' - ] + ] attrs = [ 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' - ] + ] - #load functions and attributs in tree - for fn in fns: - mandata[fn] = getattr(self,fn) - for attr in attrs: - mandata[attr] = getattr(self,attr) + # load functions and attributs in tree + for fn in fns: + mandata[fn] = getattr(self, fn) + for attr in attrs: + mandata[attr] = getattr(self, attr) # general information mandata['slug'] = slugify(mandata['title']) @@ -208,29 +207,29 @@ def load_dic(self, mandata, sha=None): and self.sha_validation == sha mandata['is_on_line'] = self.on_line() and self.sha_public == sha - #url: + # url: mandata['get_absolute_url'] = reverse( - 'zds.tutorial.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) if self.in_beta(): mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorial.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) + '?version=' + self.sha_beta + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) + '?version=' + self.sha_beta else: mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorial.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) - - mandata['get_absolute_url_online'] = reverse( - 'zds.tutorial.views.view_tutorial_online', + 'zds.tutorial.views.view_tutorial', args=[self.pk, mandata['slug']] ) + mandata['get_absolute_url_online'] = reverse( + 'zds.tutorial.views.view_tutorial_online', + args=[self.pk, mandata['slug']] + ) + def load_introduction_and_conclusion(self, mandata, sha=None, public=False): '''Explicitly load introduction and conclusion to avoid useless disk access in load_dic() @@ -242,7 +241,6 @@ def load_introduction_and_conclusion(self, mandata, sha=None, public=False): else: mandata['get_introduction'] = self.get_introduction(sha) mandata['get_conclusion'] = self.get_conclusion(sha) - def load_json_for_public(self, sha=None): if sha is None: @@ -287,7 +285,7 @@ def get_introduction(self, sha=None): if sha is None: sha = self.sha_draft repo = Repo(self.get_path()) - + manifest = get_blob(repo.commit(sha).tree, "manifest.json") tutorial_version = json_reader.loads(manifest) if "introduction" in tutorial_version: @@ -314,7 +312,7 @@ def get_conclusion(self, sha=None): if sha is None: sha = self.sha_draft repo = Repo(self.get_path()) - + manifest = get_blob(repo.commit(sha).tree, "manifest.json") tutorial_version = json_reader.loads(manifest) if "introduction" in tutorial_version: @@ -336,6 +334,17 @@ def get_conclusion_online(self): return conclu_contenu.decode('utf-8') + def delete_entity_and_tree(self): + """deletes the entity and its filesystem counterpart""" + shutil.rmtree(self.get_path(), 0) + Validation.objects.filter(tutorial=self).delete() + + if self.gallery is not None: + self.gallery.delete() + if self.on_line(): + shutil.rmtree(self.get_prod_path()) + self.delete() + def save(self, *args, **kwargs): self.slug = slugify(self.title) @@ -403,7 +412,7 @@ def antispam(self, user=None): .filter(author=user.pk)\ .order_by('-pubdate') - if last_user_notes and last_user_notes[0] == self.get_last_note(): + if last_user_notes and last_user_notes[0] == self.last_note: last_user_note = last_user_notes[0] t = timezone.now() - last_user_note.pubdate if t.total_seconds() < settings.SPAM_LIMIT_SECONDS: @@ -422,19 +431,23 @@ def have_markdown(self): return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".md")) + def have_html(self): return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".html")) + def have_pdf(self): return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".pdf")) + def have_epub(self): return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".epub")) + def get_last_tutorials(): tutorials = Tutorial.objects.all()\ .exclude(sha_public__isnull=True)\ @@ -579,14 +592,14 @@ def get_path(self, relative=False): return os.path.join(settings.REPO_PATH, self.tutorial.get_phy_slug(), self.get_phy_slug()) def get_introduction(self, sha=None): - + tutorial = self.tutorial # find hash code if sha is None: sha = tutorial.sha_draft repo = Repo(tutorial.get_path()) - + manifest = get_blob(repo.commit(sha).tree, "manifest.json") tutorial_version = json_reader.loads(manifest) if "parts" in tutorial_version: @@ -620,7 +633,7 @@ def get_conclusion(self, sha=None): if sha is None: sha = tutorial.sha_draft repo = Repo(tutorial.get_path()) - + manifest = get_blob(repo.commit(sha).tree, "manifest.json") tutorial_version = json_reader.loads(manifest) if "parts" in tutorial_version: @@ -785,13 +798,13 @@ def get_introduction(self, sha=None): if self.tutorial: tutorial = self.tutorial else: - tutorial = self.part.tutorial + tutorial = self.part.tutorial repo = Repo(tutorial.get_path()) # find hash code if sha is None: sha = tutorial.sha_draft - + manifest = get_blob(repo.commit(sha).tree, "manifest.json") tutorial_version = json_reader.loads(manifest) if "parts" in tutorial_version: @@ -800,7 +813,7 @@ def get_introduction(self, sha=None): for chapter in part["chapters"]: if chapter["pk"] == self.pk: path_chap = chapter["introduction"] - break + break if "chapter" in tutorial_version: chapter = tutorial_version["chapter"] if chapter["pk"] == self.pk: @@ -840,13 +853,13 @@ def get_conclusion(self, sha=None): if self.tutorial: tutorial = self.tutorial else: - tutorial = self.part.tutorial + tutorial = self.part.tutorial repo = Repo(tutorial.get_path()) # find hash code if sha is None: sha = tutorial.sha_draft - + manifest = get_blob(repo.commit(sha).tree, "manifest.json") tutorial_version = json_reader.loads(manifest) if "parts" in tutorial_version: @@ -855,7 +868,7 @@ def get_conclusion(self, sha=None): for chapter in part["chapters"]: if chapter["pk"] == self.pk: path_chap = chapter["conclusion"] - break + break if "chapter" in tutorial_version: chapter = tutorial_version["chapter"] if chapter["pk"] == self.pk: @@ -905,6 +918,7 @@ def update_children(self): extract.text = extract.get_path(relative=True) extract.save() + class Extract(models.Model): """A content extract from a chapter.""" @@ -962,40 +976,40 @@ def get_prod_path(self): if self.chapter.tutorial: data = self.chapter.tutorial.load_json_for_public() - mandata = tutorial.load_dic(data) + mandata = self.chapter.tutorial.load_dic(data) if "chapter" in mandata: for ext in mandata["chapter"]["extracts"]: if ext['pk'] == self.pk: return os.path.join(settings.REPO_PATH_PROD, str(self.chapter.tutorial.pk) + '_' + slugify(mandata['title']), str(ext['pk']) + "_" + slugify(ext['title'])) \ - + '.md.html' + + '.md.html' else: data = self.chapter.part.tutorial.load_json_for_public() - mandata = tutorial.load_dic(data) + mandata = self.chapter.part.tutorial.load_dic(data) for part in mandata["parts"]: for chapter in part["chapters"]: for ext in chapter["extracts"]: if ext['pk'] == self.pk: - chapter_path = os.path.join(settings.REPO_PATH_PROD, - str(mandata['pk']) + '_' + slugify(mandata['title']), - str(part['pk']) + "_" + slugify(part['title']), - str(chapter['pk']) + "_" + slugify(chapter['title']), - str(ext['pk']) + "_" + slugify(ext['title'])) \ - + '.md.html' + return os.path.join(settings.REPO_PATH_PROD, + str(mandata['pk']) + '_' + slugify(mandata['title']), + str(part['pk']) + "_" + slugify(part['title']), + str(chapter['pk']) + "_" + slugify(chapter['title']), + str(ext['pk']) + "_" + slugify(ext['title'])) \ + + '.md.html' def get_text(self, sha=None): - + if self.chapter.tutorial: tutorial = self.chapter.tutorial else: - tutorial = self.chapter.part.tutorial + tutorial = self.chapter.part.tutorial repo = Repo(tutorial.get_path()) # find hash code if sha is None: sha = tutorial.sha_draft - + manifest = get_blob(repo.commit(sha).tree, "manifest.json") tutorial_version = json_reader.loads(manifest) if "parts" in tutorial_version: @@ -1006,7 +1020,7 @@ def get_text(self, sha=None): for extract in chapter["extracts"]: if extract["pk"] == self.pk: path_ext = extract["text"] - break + break if "chapter" in tutorial_version: chapter = tutorial_version["chapter"] if "extracts" in chapter: @@ -1042,6 +1056,7 @@ def get_text_online(self): else: return None + class Validation(models.Model): """Tutorial validation.""" diff --git a/zds/tutorial/tests.py b/zds/tutorial/tests.py index 711173e98b..1d9624e82d 100644 --- a/zds/tutorial/tests.py +++ b/zds/tutorial/tests.py @@ -2,24 +2,37 @@ import os import shutil -import HTMLParser +import tempfile +import zipfile +from git import Repo +try: + import ujson as json_reader +except: + try: + import simplejson as json_reader + except: + import json as json_reader +from django.db.models import Q from django.conf import settings from django.core import mail from django.core.urlresolvers import reverse -from django.test import TestCase, RequestFactory +from django.test import TestCase from django.test.utils import override_settings -from django.utils import html +from zds.forum.factories import CategoryFactory, ForumFactory from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.gallery.factories import GalleryFactory, UserGalleryFactory, ImageFactory +from zds.gallery.factories import UserGalleryFactory, ImageFactory from zds.mp.models import PrivateTopic from zds.settings import SITE_ROOT from zds.tutorial.factories import BigTutorialFactory, MiniTutorialFactory, PartFactory, \ ChapterFactory, NoteFactory, SubCategoryFactory, LicenceFactory from zds.gallery.factories import GalleryFactory from zds.tutorial.models import Note, Tutorial, Validation, Extract, Part, Chapter +from zds.tutorial.views import insert_into_zip from zds.utils.models import SubCategory, Licence, Alert from zds.utils.misc import compute_hash + + @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) @override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) @override_settings( @@ -43,7 +56,7 @@ def setUp(self): self.user = ProfileFactory().user self.staff = StaffProfileFactory().user self.subcat = SubCategoryFactory() - + self.licence = LicenceFactory() self.licence.save() @@ -106,6 +119,7 @@ def setUp(self): reverse('zds.tutorial.views.reservation', args=[validation.pk]), follow=False) self.assertEqual(pub.status_code, 302) + self.first_validator = self.staff # publish tutorial pub = self.client.post( @@ -123,6 +137,93 @@ def setUp(self): mail.outbox = [] + def test_import_archive(self): + login_check = self.client.login( + username=self.user_author.username, + password='hostel77') + self.assertEqual(login_check, True) + # create temporary data directory + temp = os.path.join(tempfile.gettempdir(), "temp") + if not os.path.exists(temp): + os.makedirs(temp, mode=0777) + # download zip + repo_path = os.path.join(settings.REPO_PATH, self.bigtuto.get_phy_slug()) + repo = Repo(repo_path) + zip_path = os.path.join(tempfile.gettempdir(), self.bigtuto.slug + '.zip') + zip_file = zipfile.ZipFile(zip_path, 'w') + insert_into_zip(zip_file, repo.commit(self.bigtuto.sha_draft).tree) + zip_file.close() + + zip_dir = os.path.join(temp, self.bigtuto.get_phy_slug()) + if not os.path.exists(zip_dir): + os.makedirs(zip_dir, mode=0777) + + # Extract zip + with zipfile.ZipFile(zip_path) as zip_file: + for member in zip_file.namelist(): + filename = os.path.basename(member) + # skip directories + if not filename: + continue + if not os.path.exists(os.path.dirname(os.path.join(zip_dir, member))): + os.makedirs(os.path.dirname(os.path.join(zip_dir, member)), mode=0777) + # copy file (taken from zipfile's extract) + source = zip_file.open(member) + target = file(os.path.join(zip_dir, filename), "wb") + with source, target: + shutil.copyfileobj(source, target) + self.assertTrue(os.path.isdir(zip_dir)) + + # update markdown files + up_intro_tfile = open(os.path.join(temp, self.bigtuto.get_phy_slug(), self.bigtuto.introduction), "a") + up_intro_tfile.write(u"preuve de modification de l'introduction") + up_intro_tfile.close() + up_conclu_tfile = open(os.path.join(temp, self.bigtuto.get_phy_slug(), self.bigtuto.conclusion), "a") + up_conclu_tfile.write(u"preuve de modification de la conclusion") + up_conclu_tfile.close() + parts = Part.objects.filter(tutorial__pk=self.bigtuto.pk) + for part in parts: + up_intro_pfile = open(os.path.join(temp, self.bigtuto.get_phy_slug(), part.introduction), "a") + up_intro_pfile.write(u"preuve de modification de l'introduction") + up_intro_pfile.close() + up_conclu_pfile = open(os.path.join(temp, self.bigtuto.get_phy_slug(), part.conclusion), "a") + up_conclu_pfile.write(u"preuve de modification de la conclusion") + up_conclu_pfile.close() + chapters = Chapter.objects.filter(part__pk=part.pk) + for chapter in chapters: + up_intro_cfile = open(os.path.join(temp, self.bigtuto.get_phy_slug(), chapter.introduction), "a") + up_intro_cfile.write(u"preuve de modification de l'introduction") + up_intro_cfile.close() + up_conclu_cfile = open(os.path.join(temp, self.bigtuto.get_phy_slug(), chapter.conclusion), "a") + up_conclu_cfile.write(u"preuve de modification de la conclusion") + up_conclu_cfile.close() + + # zip directory + shutil.make_archive(os.path.join(temp, self.bigtuto.get_phy_slug()), + "zip", + os.path.join(temp, self.bigtuto.get_phy_slug())) + + self.assertTrue(os.path.isfile(os.path.join(temp, self.bigtuto.get_phy_slug()+".zip"))) + + # import zip archive + result = self.client.post( + reverse('zds.tutorial.views.import_tuto'), + { + 'file': open( + os.path.join( + temp, + os.path.join(temp, self.bigtuto.get_phy_slug()+".zip")), + 'r'), + 'tutorial': self.bigtuto.pk, + 'import-archive': "importer"}, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Tutorial.objects.all().count(), 1) + + # delete temporary data directory + shutil.rmtree(temp) + os.remove(zip_path) + def test_add_note(self): """To test add note for tutorial.""" user1 = ProfileFactory().user @@ -402,7 +503,8 @@ def test_import_tuto(self): 'tuto', 'temps-reel-avec-irrlicht', 'images.zip'), - 'r')}, + 'r'), + 'import-tuto': "importer"}, follow=False) self.assertEqual(result.status_code, 302) @@ -477,10 +579,14 @@ def test_url_for_guest(self): self.chapter2_1.slug]), follow=False) self.assertEqual(result.status_code, 302) - + def test_workflow_tuto(self): """Test workflow of tutorial.""" - + + ForumFactory( + category=CategoryFactory(position=1), + position_in_category=1) + # logout before self.client.logout() # login with simple member @@ -489,38 +595,38 @@ def test_workflow_tuto(self): username=self.user.username, password='hostel77'), True) - - #add new big tuto + + # add new big tuto result = self.client.post( reverse('zds.tutorial.views.add_tutorial'), { 'title': u"Introduction à l'algèbre", 'description': "Perçer les mystère de boole", - 'introduction':"Bienvenue dans le monde binaires", + 'introduction': "Bienvenue dans le monde binaires", 'conclusion': "", 'type': "BIG", - 'licence' : self.licence.pk, + 'licence': self.licence.pk, 'subcategory': self.subcat.pk, }, follow=False) - + self.assertEqual(result.status_code, 302) self.assertEqual(Tutorial.objects.all().count(), 2) tuto = Tutorial.objects.last() - #add part 1 + # add part 1 result = self.client.post( reverse('zds.tutorial.views.add_part') + '?tutoriel={}'.format(tuto.pk), { 'title': u"Partie 1", - 'introduction':u"Présentation", + 'introduction': u"Présentation", 'conclusion': u"Fin de la présentation", }, follow=False) self.assertEqual(result.status_code, 302) self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 1) p1 = Part.objects.filter(tutorial=tuto).last() - - #check view offline + + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_part', @@ -530,10 +636,10 @@ def test_workflow_tuto(self): p1.pk, p1.slug]), follow=True) - self.assertContains(response=result, text = u"Présentation") - self.assertContains(response=result, text = u"Fin de la présentation") - - #add part 2 + self.assertContains(response=result, text=u"Présentation") + self.assertContains(response=result, text=u"Fin de la présentation") + + # add part 2 result = self.client.post( reverse('zds.tutorial.views.add_part') + '?tutoriel={}'.format(tuto.pk), { @@ -546,7 +652,7 @@ def test_workflow_tuto(self): self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 2) p2 = Part.objects.filter(tutorial=tuto).last() self.assertEqual(u"Analyse", p2.get_introduction()) - #check view offline + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_part', @@ -556,23 +662,23 @@ def test_workflow_tuto(self): p2.pk, p2.slug]), follow=True) - self.assertContains(response=result, text = u"Analyse") - self.assertContains(response=result, text = u"Fin de l'analyse") + self.assertContains(response=result, text=u"Analyse") + self.assertContains(response=result, text=u"Fin de l'analyse") - #add part 3 + # add part 3 result = self.client.post( reverse('zds.tutorial.views.add_part') + '?tutoriel={}'.format(tuto.pk), { 'title': u"Partie 2", - 'introduction':"Expérimentation", + 'introduction': "Expérimentation", 'conclusion': "C'est terminé", }, follow=False) self.assertEqual(result.status_code, 302) self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 3) p3 = Part.objects.filter(tutorial=tuto).last() - - #check view offline + + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_part', @@ -582,15 +688,15 @@ def test_workflow_tuto(self): p3.pk, p3.slug]), follow=True) - self.assertContains(response=result, text = u"Expérimentation") - self.assertContains(response=result, text = u"C'est terminé") + self.assertContains(response=result, text=u"Expérimentation") + self.assertContains(response=result, text=u"C'est terminé") - #add chapter 1 for part 2 + # add chapter 1 for part 2 result = self.client.post( reverse('zds.tutorial.views.add_chapter') + '?partie={}'.format(p2.pk), { 'title': u"Chapitre 1", - 'introduction':"Mon premier chapitre", + 'introduction': "Mon premier chapitre", 'conclusion': "Fin de mon premier chapitre", }, follow=False) @@ -599,8 +705,8 @@ def test_workflow_tuto(self): self.assertEqual(Chapter.objects.filter(part=p2).count(), 1) self.assertEqual(Chapter.objects.filter(part=p3).count(), 0) c1 = Chapter.objects.filter(part=p2).last() - - #check view offline + + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_chapter', @@ -612,16 +718,16 @@ def test_workflow_tuto(self): c1.pk, c1.slug]), follow=True) - self.assertContains(response=result, text = u"Mon premier chapitre") - self.assertContains(response=result, text = u"Fin de mon premier chapitre") + self.assertContains(response=result, text=u"Mon premier chapitre") + self.assertContains(response=result, text=u"Fin de mon premier chapitre") - #add chapter 2 for part 2 + # add chapter 2 for part 2 result = self.client.post( reverse('zds.tutorial.views.add_chapter') + '?partie={}'.format(p2.pk), { 'title': u"Chapitre 2", 'introduction': u"Mon deuxième chapitre", - 'conclusion':u"Fin de mon deuxième chapitre", + 'conclusion': u"Fin de mon deuxième chapitre", }, follow=False) self.assertEqual(result.status_code, 302) @@ -629,8 +735,8 @@ def test_workflow_tuto(self): self.assertEqual(Chapter.objects.filter(part=p2).count(), 2) self.assertEqual(Chapter.objects.filter(part=p3).count(), 0) c2 = Chapter.objects.filter(part=p2).last() - - #check view offline + + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_chapter', @@ -642,10 +748,10 @@ def test_workflow_tuto(self): c2.pk, c2.slug]), follow=True) - self.assertContains(response=result, text = u"Mon deuxième chapitre") - self.assertContains(response=result, text = u"Fin de mon deuxième chapitre") - - #add chapter 3 for part 2 + self.assertContains(response=result, text=u"Mon deuxième chapitre") + self.assertContains(response=result, text=u"Fin de mon deuxième chapitre") + + # add chapter 3 for part 2 result = self.client.post( reverse('zds.tutorial.views.add_chapter') + '?partie={}'.format(p2.pk), { @@ -659,8 +765,8 @@ def test_workflow_tuto(self): self.assertEqual(Chapter.objects.filter(part=p2).count(), 3) self.assertEqual(Chapter.objects.filter(part=p3).count(), 0) c3 = Chapter.objects.filter(part=p2).last() - - #check view offline + + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_chapter', @@ -672,15 +778,15 @@ def test_workflow_tuto(self): c3.pk, c3.slug]), follow=True) - self.assertContains(response=result, text = u"Mon troisième chapitre homonyme") - self.assertContains(response=result, text = u"Fin de mon troisième chapitre") - - #add chapter 4 for part 1 + self.assertContains(response=result, text=u"Mon troisième chapitre homonyme") + self.assertContains(response=result, text=u"Fin de mon troisième chapitre") + + # add chapter 4 for part 1 result = self.client.post( reverse('zds.tutorial.views.add_chapter') + '?partie={}'.format(p1.pk), { 'title': u"Chapitre 1", - 'introduction':"Mon premier chapitre d'une autre partie", + 'introduction': "Mon premier chapitre d'une autre partie", 'conclusion': "", }, follow=False) @@ -689,8 +795,8 @@ def test_workflow_tuto(self): self.assertEqual(Chapter.objects.filter(part=p2).count(), 3) self.assertEqual(Chapter.objects.filter(part=p3).count(), 0) c4 = Chapter.objects.filter(part=p1).last() - - #check view offline + + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_chapter', @@ -702,14 +808,14 @@ def test_workflow_tuto(self): c4.pk, c4.slug]), follow=True) - self.assertContains(response=result, text = u"Mon premier chapitre d'une autre partie") - + self.assertContains(response=result, text=u"Mon premier chapitre d'une autre partie") + # add extract 1 of chapter 3 result = self.client.post( reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(c3.pk), { 'title': u"Extrait 1", - 'text':"Prune", + 'text': "Prune", }, follow=False) self.assertEqual(result.status_code, 302) @@ -721,7 +827,7 @@ def test_workflow_tuto(self): reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(c3.pk), { 'title': u"Extrait 2", - 'text':"Citron", + 'text': "Citron", }, follow=False) self.assertEqual(result.status_code, 302) @@ -733,80 +839,80 @@ def test_workflow_tuto(self): reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(c2.pk), { 'title': u"Extrait 3", - 'text':"Kiwi", + 'text': "Kiwi", }, follow=False) self.assertEqual(result.status_code, 302) self.assertEqual(Extract.objects.filter(chapter=c2).count(), 1) e3 = Extract.objects.filter(chapter=c2).last() - #check content edit part + # check content edit part result = self.client.get( - reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p1.pk), + reverse('zds.tutorial.views.edit_part') + "?partie={}".format(p1.pk), follow=True) - self.assertContains(response=result, text = u"Présentation") - self.assertContains(response=result, text = u"Fin de la présentation") - + self.assertContains(response=result, text=u"Présentation") + self.assertContains(response=result, text=u"Fin de la présentation") + result = self.client.get( - reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p2.pk), + reverse('zds.tutorial.views.edit_part') + "?partie={}".format(p2.pk), follow=True) - self.assertContains(response=result, text = u"Analyse") - self.assertContains(response=result, text = "Fin de l'analyse") + self.assertContains(response=result, text=u"Analyse") + self.assertContains(response=result, text="Fin de l'analyse") result = self.client.get( - reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p3.pk), + reverse('zds.tutorial.views.edit_part') + "?partie={}".format(p3.pk), follow=True) - self.assertContains(response=result, text = u"Expérimentation") - self.assertContains(response=result, text = u"est terminé") - - #check content edit chapter + self.assertContains(response=result, text=u"Expérimentation") + self.assertContains(response=result, text=u"est terminé") + + # check content edit chapter result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c1.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c1.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 1") - self.assertContains(response=result, text = u"Mon premier chapitre") - self.assertContains(response=result, text = u"Fin de mon premier chapitre") - + self.assertContains(response=result, text=u"Chapitre 1") + self.assertContains(response=result, text=u"Mon premier chapitre") + self.assertContains(response=result, text=u"Fin de mon premier chapitre") + result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c2.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c2.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 2") - self.assertContains(response=result, text = u"Mon deuxième chapitre") - self.assertContains(response=result, text = u"Fin de mon deuxième chapitre") + self.assertContains(response=result, text=u"Chapitre 2") + self.assertContains(response=result, text=u"Mon deuxième chapitre") + self.assertContains(response=result, text=u"Fin de mon deuxième chapitre") result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c3.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c3.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 2") - self.assertContains(response=result, text = u"Mon troisième chapitre homonyme") - self.assertContains(response=result, text = u"Fin de mon troisième chapitre") + self.assertContains(response=result, text=u"Chapitre 2") + self.assertContains(response=result, text=u"Mon troisième chapitre homonyme") + self.assertContains(response=result, text=u"Fin de mon troisième chapitre") result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c4.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c4.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 1") - self.assertContains(response=result, text = u"Mon premier chapitre d'une autre partie") + self.assertContains(response=result, text=u"Chapitre 1") + self.assertContains(response=result, text=u"Mon premier chapitre d'une autre partie") - #check content edit extract + # check content edit extract result = self.client.get( - reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e1.pk), + reverse('zds.tutorial.views.edit_extract') + "?extrait={}".format(e1.pk), follow=True) - self.assertContains(response=result, text = u"Extrait 1") - self.assertContains(response=result, text = u"Prune") + self.assertContains(response=result, text=u"Extrait 1") + self.assertContains(response=result, text=u"Prune") result = self.client.get( - reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e2.pk), + reverse('zds.tutorial.views.edit_extract') + "?extrait={}".format(e2.pk), follow=True) - self.assertContains(response=result, text = u"Extrait 2") - self.assertContains(response=result, text = u"Citron") + self.assertContains(response=result, text=u"Extrait 2") + self.assertContains(response=result, text=u"Citron") result = self.client.get( - reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e3.pk), + reverse('zds.tutorial.views.edit_extract') + "?extrait={}".format(e3.pk), follow=True) - self.assertContains(response=result, text = u"Extrait 3") - self.assertContains(response=result, text = u"Kiwi") + self.assertContains(response=result, text=u"Extrait 3") + self.assertContains(response=result, text=u"Kiwi") - #edit part 2 + # edit part 2 result = self.client.post( reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p2.pk), { @@ -814,32 +920,32 @@ def test_workflow_tuto(self): 'introduction': u"Expérimentation : edition d'introduction", 'conclusion': u"C'est terminé : edition de conlusion", "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), - os.path.join(p2.tutorial.get_path(), p2.conclusion)]) + os.path.join(p2.tutorial.get_path(), p2.conclusion)]) }, follow=True) - self.assertContains(response=result, text = u"Partie 2 : edition de titre") - self.assertContains(response=result, text = u"Expérimentation : edition d'introduction") - self.assertContains(response=result, text = u"C'est terminé : edition de conlusion") + self.assertContains(response=result, text=u"Partie 2 : edition de titre") + self.assertContains(response=result, text=u"Expérimentation : edition d'introduction") + self.assertContains(response=result, text=u"C'est terminé : edition de conlusion") self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 3) - #edit chapter 3 + # edit chapter 3 result = self.client.post( reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c3.pk), { 'title': u"Chapitre 3 : edition de titre", 'introduction': u"Edition d'introduction", 'conclusion': u"Edition de conlusion", - "last_hash": compute_hash([os.path.join(c3.get_path(),"introduction.md"), - os.path.join(c3.get_path(),"conclusion.md")]) + "last_hash": compute_hash([os.path.join(c3.get_path(), "introduction.md"), + os.path.join(c3.get_path(), "conclusion.md")]) }, follow=True) - self.assertContains(response=result, text = u"Chapitre 3 : edition de titre") - self.assertContains(response=result, text = u"Edition d'introduction") - self.assertContains(response=result, text = u"Edition de conlusion") + self.assertContains(response=result, text=u"Chapitre 3 : edition de titre") + self.assertContains(response=result, text=u"Edition d'introduction") + self.assertContains(response=result, text=u"Edition de conlusion") self.assertEqual(Chapter.objects.filter(part=p2.pk).count(), 3) p2 = Part.objects.filter(pk=p2.pk).first() - #edit part 2 + # edit part 2 result = self.client.post( reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p2.pk), { @@ -847,31 +953,31 @@ def test_workflow_tuto(self): 'introduction': u"Expérimentation : seconde edition d'introduction", 'conclusion': u"C'est terminé : seconde edition de conlusion", "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), - os.path.join(p2.tutorial.get_path(), p2.conclusion)]) + os.path.join(p2.tutorial.get_path(), p2.conclusion)]) }, follow=True) - self.assertContains(response=result, text = u"Partie 2 : seconde edition de titre") - self.assertContains(response=result, text = u"Expérimentation : seconde edition d'introduction") - self.assertContains(response=result, text = u"C'est terminé : seconde edition de conlusion") + self.assertContains(response=result, text=u"Partie 2 : seconde edition de titre") + self.assertContains(response=result, text=u"Expérimentation : seconde edition d'introduction") + self.assertContains(response=result, text=u"C'est terminé : seconde edition de conlusion") self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 3) - - #edit chapter 2 + + # edit chapter 2 result = self.client.post( reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c2.pk), { 'title': u"Chapitre 2 : edition de titre", 'introduction': u"Edition d'introduction", 'conclusion': u"Edition de conlusion", - "last_hash": compute_hash([os.path.join(c2.get_path(),"introduction.md"), - os.path.join(c2.get_path(),"conclusion.md")]) + "last_hash": compute_hash([os.path.join(c2.get_path(), "introduction.md"), + os.path.join(c2.get_path(), "conclusion.md")]) }, follow=True) - self.assertContains(response=result, text = u"Chapitre 2 : edition de titre") - self.assertContains(response=result, text = u"Edition d'introduction") - self.assertContains(response=result, text = u"Edition de conlusion") + self.assertContains(response=result, text=u"Chapitre 2 : edition de titre") + self.assertContains(response=result, text=u"Edition d'introduction") + self.assertContains(response=result, text=u"Edition de conlusion") self.assertEqual(Chapter.objects.filter(part=p2.pk).count(), 3) - #edit extract 2 + # edit extract 2 result = self.client.post( reverse('zds.tutorial.views.edit_extract') + '?extrait={}'.format(e2.pk), { @@ -880,77 +986,77 @@ def test_workflow_tuto(self): "last_hash": compute_hash([os.path.join(e2.get_path())]) }, follow=True) - self.assertContains(response=result, text = u"Extrait 2 : edition de titre") - self.assertContains(response=result, text = u"Agrume") + self.assertContains(response=result, text=u"Extrait 2 : edition de titre") + self.assertContains(response=result, text=u"Agrume") - #check content edit part + # check content edit part result = self.client.get( - reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p1.pk), + reverse('zds.tutorial.views.edit_part') + "?partie={}".format(p1.pk), follow=True) - self.assertContains(response=result, text = u"Présentation") - self.assertContains(response=result, text = u"Fin de la présentation") - + self.assertContains(response=result, text=u"Présentation") + self.assertContains(response=result, text=u"Fin de la présentation") + result = self.client.get( - reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p2.pk), + reverse('zds.tutorial.views.edit_part') + "?partie={}".format(p2.pk), follow=True) - self.assertContains(response=result, text = u"Partie 2 : seconde edition de titre") - self.assertContains(response=result, text = "Expérimentation : seconde edition d'introduction") - self.assertContains(response=result, text = "C'est terminé : seconde edition de conlusion") + self.assertContains(response=result, text=u"Partie 2 : seconde edition de titre") + self.assertContains(response=result, text="Expérimentation : seconde edition d'introduction") + self.assertContains(response=result, text="C'est terminé : seconde edition de conlusion") result = self.client.get( - reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p3.pk), + reverse('zds.tutorial.views.edit_part') + "?partie={}".format(p3.pk), follow=True) - self.assertContains(response=result, text = u"Expérimentation") - self.assertContains(response=result, text = u"est terminé") - - #check content edit chapter + self.assertContains(response=result, text=u"Expérimentation") + self.assertContains(response=result, text=u"est terminé") + + # check content edit chapter result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c1.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c1.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 1") - self.assertContains(response=result, text = u"Mon premier chapitre") - self.assertContains(response=result, text = u"Fin de mon premier chapitre") + self.assertContains(response=result, text=u"Chapitre 1") + self.assertContains(response=result, text=u"Mon premier chapitre") + self.assertContains(response=result, text=u"Fin de mon premier chapitre") result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c2.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c2.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 2 : edition de titre") - self.assertContains(response=result, text = u"Edition d'introduction") - self.assertContains(response=result, text = u"Edition de conlusion") + self.assertContains(response=result, text=u"Chapitre 2 : edition de titre") + self.assertContains(response=result, text=u"Edition d'introduction") + self.assertContains(response=result, text=u"Edition de conlusion") result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c3.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c3.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 3 : edition de titre") - self.assertContains(response=result, text = u"Edition d'introduction") - self.assertContains(response=result, text = u"Edition de conlusion") + self.assertContains(response=result, text=u"Chapitre 3 : edition de titre") + self.assertContains(response=result, text=u"Edition d'introduction") + self.assertContains(response=result, text=u"Edition de conlusion") result = self.client.get( - reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c4.pk), + reverse('zds.tutorial.views.edit_chapter') + "?chapitre={}".format(c4.pk), follow=True) - self.assertContains(response=result, text = u"Chapitre 1") - self.assertContains(response=result, text = u"Mon premier chapitre d'une autre partie") + self.assertContains(response=result, text=u"Chapitre 1") + self.assertContains(response=result, text=u"Mon premier chapitre d'une autre partie") - #check content edit extract + # check content edit extract result = self.client.get( - reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e1.pk), + reverse('zds.tutorial.views.edit_extract') + "?extrait={}".format(e1.pk), follow=True) - self.assertContains(response=result, text = u"Extrait 1") - self.assertContains(response=result, text = u"Prune") + self.assertContains(response=result, text=u"Extrait 1") + self.assertContains(response=result, text=u"Prune") result = self.client.get( - reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e2.pk), + reverse('zds.tutorial.views.edit_extract') + "?extrait={}".format(e2.pk), follow=True) - self.assertContains(response=result, text = u"Extrait 2 : edition de titre") - self.assertContains(response=result, text = u"Agrume") + self.assertContains(response=result, text=u"Extrait 2 : edition de titre") + self.assertContains(response=result, text=u"Agrume") result = self.client.get( - reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e3.pk), + reverse('zds.tutorial.views.edit_extract') + "?extrait={}".format(e3.pk), follow=True) - self.assertContains(response=result, text = u"Extrait 3") - self.assertContains(response=result, text = u"Kiwi") - - #move chapter 1 against 2 + self.assertContains(response=result, text=u"Extrait 3") + self.assertContains(response=result, text=u"Kiwi") + + # move chapter 1 against 2 result = self.client.post( reverse('zds.tutorial.views.modify_chapter'), { @@ -958,7 +1064,7 @@ def test_workflow_tuto(self): 'move_target': c2.position_in_part, }, follow=True) - #move part 1 against 2 + # move part 1 against 2 result = self.client.post( reverse('zds.tutorial.views.modify_part'), { @@ -967,9 +1073,10 @@ def test_workflow_tuto(self): }, follow=True) self.assertEqual(Chapter.objects.filter(part__tutorial=tuto.pk).count(), 4) - + # ask public tutorial tuto = Tutorial.objects.get(pk=tuto.pk) + pub = self.client.post( reverse('zds.tutorial.views.ask_validation'), { @@ -997,6 +1104,49 @@ def test_workflow_tuto(self): reverse('zds.tutorial.views.reservation', args=[validation.pk]), follow=False) self.assertEqual(pub.status_code, 302) + old_validator = self.staff + old_mps_count = PrivateTopic.objects\ + .filter(Q(author=old_validator) | Q(participants__in=[old_validator]))\ + .count() + + # logout staff before + self.client.logout() + # login with simple member + self.assertEqual( + self.client.login( + username=self.user.username, + password='hostel77'), + True) + + # ask public tutorial again + pub = self.client.post( + reverse('zds.tutorial.views.ask_validation'), + { + 'tutorial': tuto.pk, + 'text': u'Nouvelle demande de publication', + 'version': tuto.sha_draft, + 'source': 'www.zestedesavoir.com', + }, + follow=False) + self.assertEqual(pub.status_code, 302) + + # old validator stay + validation = Validation.objects.filter(tutorial__pk=tuto.pk).last() + self.assertEqual(old_validator, validation.validator) + + # new MP for staff + new_mps_count = PrivateTopic.objects\ + .filter(Q(author=old_validator) | Q(participants__in=[old_validator]))\ + .count() + self.assertEqual((new_mps_count - old_mps_count), 1) + # logout before + self.client.logout() + # login with staff member + self.assertEqual( + self.client.login( + username=self.staff.username, + password='hostel77'), + True) # publish tutorial pub = self.client.post( @@ -1008,23 +1158,23 @@ def test_workflow_tuto(self): 'source': 'http://zestedesavoir.com', }, follow=False) - + # then active the beta on tutorial : sha_draft = Tutorial.objects.get(pk=tuto.pk).sha_draft response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': tuto.pk, - 'activ_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': tuto.pk, + 'activ_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) sha_beta = Tutorial.objects.get(pk=tuto.pk).sha_beta self.assertEqual(sha_draft, sha_beta) - #delete part 1 + # delete part 1 result = self.client.post( reverse('zds.tutorial.views.modify_part'), { @@ -1032,7 +1182,7 @@ def test_workflow_tuto(self): 'delete': "OK", }, follow=True) - #delete chapter 3 + # delete chapter 3 result = self.client.post( reverse('zds.tutorial.views.modify_chapter'), { @@ -1043,7 +1193,7 @@ def test_workflow_tuto(self): self.assertEqual(Chapter.objects.filter(part__tutorial=tuto.pk).count(), 2) self.assertEqual(Part.objects.filter(tutorial=tuto.pk).count(), 2) - #check view delete part and chapter (draft version) + # check view delete part and chapter (draft version) result = self.client.get( reverse( 'zds.tutorial.views.view_part', @@ -1068,7 +1218,7 @@ def test_workflow_tuto(self): follow=True) self.assertEqual(result.status_code, 404) - #deleted part and section HAVE TO be accessible on beta (get 200) + # deleted part and section HAVE TO be accessible on beta (get 200) result = self.client.get( reverse( 'zds.tutorial.views.view_part', @@ -1093,7 +1243,7 @@ def test_workflow_tuto(self): follow=True) self.assertEqual(result.status_code, 200) - #deleted part and section HAVE TO be accessible online (get 200) + # deleted part and section HAVE TO be accessible online (get 200) result = self.client.get( reverse( 'zds.tutorial.views.view_part_online', @@ -1117,7 +1267,7 @@ def test_workflow_tuto(self): c3.slug]), follow=True) self.assertEqual(result.status_code, 200) - + # ask public tutorial tuto = Tutorial.objects.get(pk=tuto.pk) pub = self.client.post( @@ -1149,7 +1299,7 @@ def test_workflow_tuto(self): 'source': 'http://zestedesavoir.com', }, follow=False) - #check view delete part and chapter (draft version, get 404) + # check view delete part and chapter (draft version, get 404) result = self.client.get( reverse( 'zds.tutorial.views.view_part', @@ -1174,7 +1324,7 @@ def test_workflow_tuto(self): follow=True) self.assertEqual(result.status_code, 404) - #deleted part and section no longer accessible online (get 404) + # deleted part and section no longer accessible online (get 404) result = self.client.get( reverse( 'zds.tutorial.views.view_part_online', @@ -1199,7 +1349,7 @@ def test_workflow_tuto(self): follow=True) self.assertEqual(result.status_code, 404) - #deleted part and section still accessible on beta (get 200) + # deleted part and section still accessible on beta (get 200) result = self.client.get( reverse( 'zds.tutorial.views.view_part', @@ -1229,7 +1379,7 @@ def test_conflict_does_not_destroy(self): sub = SubCategory() sub.title = "toto" sub.save() - # logout before + # logout before self.client.logout() # first, login with author : self.assertEqual( @@ -1238,36 +1388,40 @@ def test_conflict_does_not_destroy(self): password='hostel77'), True) # test tuto - (introduction_path, conclusion_path) =(os.path.join(self.bigtuto.get_path(),"introduction.md"), os.path.join(self.bigtuto.get_path(),"conclusion.md")) + (introduction_path, + conclusion_path) = (os.path.join(self.bigtuto.get_path(), + "introduction.md"), + os.path.join(self.bigtuto.get_path(), + "conclusion.md")) hash = compute_hash([introduction_path, conclusion_path]) self.client.post( - reverse('zds.tutorial.views.edit_tutorial')+'?tutoriel={0}'.format(self.bigtuto.pk), + reverse('zds.tutorial.views.edit_tutorial') + '?tutoriel={0}'.format(self.bigtuto.pk), { 'title': self.bigtuto.title, 'description': "nouvelle description", 'subcategory': [sub.pk], - 'introduction': self.bigtuto.get_introduction() +" un essai", + 'introduction': self.bigtuto.get_introduction() + " un essai", 'conclusion': self.bigtuto.get_conclusion(), - 'licence' : self.bigtuto.licence.pk, - 'last_hash': hash - }, follow= True) + 'licence': self.bigtuto.licence.pk, + 'last_hash': hash + }, follow=True) conflict_result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial')+'?tutoriel={0}'.format(self.bigtuto.pk), + reverse('zds.tutorial.views.edit_tutorial') + '?tutoriel={0}'.format(self.bigtuto.pk), { 'title': self.bigtuto.title, 'description': "nouvelle description", 'subcategory': [sub.pk], - 'introduction': self.bigtuto.get_introduction() +" conflictual", + 'introduction': self.bigtuto.get_introduction() + " conflictual", 'conclusion': self.bigtuto.get_conclusion(), - 'licence' : self.bigtuto.licence.pk, - 'last_hash': hash - }, follow= False) + 'licence': self.bigtuto.licence.pk, + 'last_hash': hash + }, follow=False) self.assertEqual(conflict_result.status_code, 200) - self.assertContains(response=conflict_result, text = u"nouvelle version") + self.assertContains(response=conflict_result, text=u"nouvelle version") # test parts - result = self.client.post( + self.client.post( reverse('zds.tutorial.views.add_part') + '?tutoriel={}'.format(self.bigtuto.pk), { 'title': u"Partie 2", @@ -1277,7 +1431,7 @@ def test_conflict_does_not_destroy(self): follow=False) p1 = Part.objects.last() hash = compute_hash([os.path.join(p1.tutorial.get_path(), p1.introduction), - os.path.join(p1.tutorial.get_path(), p1.conclusion)]) + os.path.join(p1.tutorial.get_path(), p1.conclusion)]) self.client.post( reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p1.pk), { @@ -1297,20 +1451,20 @@ def test_conflict_does_not_destroy(self): }, follow=False) self.assertEqual(conflict_result.status_code, 200) - self.assertContains(response=conflict_result, text = u"nouvelle version") + self.assertContains(response=conflict_result, text=u"nouvelle version") # test chapter - result = self.client.post( + self.client.post( reverse('zds.tutorial.views.add_chapter') + '?partie={}'.format(p1.pk), { 'title': u"Chapitre 1", - 'introduction':"Mon premier chapitre", + 'introduction': "Mon premier chapitre", 'conclusion': "Fin de mon premier chapitre", }, follow=False) c1 = Chapter.objects.last() - hash = compute_hash([os.path.join(c1.get_path(),"introduction.md"), - os.path.join(c1.get_path(),"conclusion.md")]) + hash = compute_hash([os.path.join(c1.get_path(), "introduction.md"), + os.path.join(c1.get_path(), "conclusion.md")]) self.client.post( reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c1.pk), { @@ -1330,7 +1484,7 @@ def test_conflict_does_not_destroy(self): }, follow=True) self.assertEqual(conflict_result.status_code, 200) - self.assertContains(response=conflict_result, text = u"nouvelle version") + self.assertContains(response=conflict_result, text=u"nouvelle version") def test_url_for_member(self): """Test simple get request by simple member.""" @@ -1665,42 +1819,44 @@ def test_delete_image_tutorial(self): # Attach an image of a gallery at a tutorial. image_tutorial = ImageFactory(gallery=self.bigtuto.gallery) - user_gallery = UserGalleryFactory(user=self.user_author, gallery=self.bigtuto.gallery) + UserGalleryFactory(user=self.user_author, gallery=self.bigtuto.gallery) self.bigtuto.image = image_tutorial self.bigtuto.save() - self.assertTrue(Tutorial.objects.get(pk=self.bigtuto.pk).image != None) + self.assertTrue(Tutorial.objects.get(pk=self.bigtuto.pk).image is not None) # Delete the image of the bigtuto. response = self.client.post( - reverse('zds.gallery.views.delete_image'), - { - 'gallery': self.bigtuto.gallery.pk, - 'delete_multi': '', - 'items': [image_tutorial.pk] - }, - follow=True + reverse('zds.gallery.views.delete_image'), + { + 'gallery': self.bigtuto.gallery.pk, + 'delete_multi': '', + 'items': [image_tutorial.pk] + }, + follow=True ) self.assertEqual(200, response.status_code) # Check if the tutorial is already in database and it doesn't have image. self.assertEqual(1, Tutorial.objects.filter(pk=self.bigtuto.pk).count()) - self.assertTrue(Tutorial.objects.get(pk=self.bigtuto.pk).image == None) + self.assertTrue(Tutorial.objects.get(pk=self.bigtuto.pk).image is None) - def test_workflow_beta_tuto(self) : + def test_workflow_beta_tuto(self): "Ensure the behavior of the beta version of tutorials" - + ForumFactory( + category=CategoryFactory(position=1), + position_in_category=1) # logout before self.client.logout() # check if acess to page with beta tutorial (with guest) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) self.assertEqual(200, response.status_code) @@ -1713,37 +1869,37 @@ def test_workflow_beta_tuto(self) : # check if acess to page with beta tutorial (with author) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) self.assertEqual(200, response.status_code) - + # then active the beta on tutorial : sha_draft = Tutorial.objects.get(pk=self.bigtuto.pk).sha_draft response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.bigtuto.pk, - 'activ_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.bigtuto.pk, + 'activ_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) # check if acess to page with beta tutorial (with author) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) self.assertEqual(200, response.status_code) # test beta : self.assertEqual( - Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, + Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, sha_draft) url = Tutorial.objects.get(pk=self.bigtuto.pk).get_absolute_url_beta() sha_beta = sha_draft @@ -1758,10 +1914,10 @@ def test_workflow_beta_tuto(self) : 302) # check if acess to page with beta tutorial (with guest, get 200) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) self.assertEqual(200, response.status_code) # test tutorial acess for random user @@ -1772,16 +1928,16 @@ def test_workflow_beta_tuto(self) : True) # check if acess to page with beta tutorial (with random user, get 200) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) # test access (get 200) self.assertEqual( self.client.get(url).status_code, 200) - + # then modify tutorial self.assertEqual( self.client.login( @@ -1789,32 +1945,32 @@ def test_workflow_beta_tuto(self) : password='hostel77'), True) result = self.client.post( - reverse( 'zds.tutorial.views.add_extract') + + reverse('zds.tutorial.views.add_extract') + '?chapitre={0}'.format( self.chapter2_1.pk), { - 'title' : "Introduction", - 'text' : u"Le contenu de l'extrait" + 'title': "Introduction", + 'text': u"Le contenu de l'extrait" }) - + self.assertEqual(result.status_code, 302) self.assertEqual( - Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, + Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, sha_beta) self.assertNotEqual( - Tutorial.objects.get(pk=self.bigtuto.pk).sha_draft, + Tutorial.objects.get(pk=self.bigtuto.pk).sha_draft, sha_beta) # update beta sha_draft = Tutorial.objects.get(pk=self.bigtuto.pk).sha_draft response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.bigtuto.pk, - 'update_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.bigtuto.pk, + 'update_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) old_url = url @@ -1843,17 +1999,17 @@ def test_workflow_beta_tuto(self) : password='hostel77'), True) response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.bigtuto.pk, - 'desactiv_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.bigtuto.pk, + 'desactiv_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) self.assertEqual( - Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, + Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, None) # test access from outside (get 302, connexion form) self.client.logout() @@ -1877,13 +2033,13 @@ def test_workflow_beta_tuto(self) : password='hostel77'), True) response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.bigtuto.pk, - 'activ_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.bigtuto.pk, + 'activ_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) # test access from outside (get 302, connexion form) @@ -1908,7 +2064,7 @@ def test_gallery_tuto_change_name(self): """ newtitle = u"The New Title" - response = self.client.post( + self.client.post( reverse('zds.tutorial.views.edit_tutorial') + '?tutoriel={}'.format(self.bigtuto.pk), { 'title': newtitle, @@ -1916,9 +2072,9 @@ def test_gallery_tuto_change_name(self): 'introduction': self.bigtuto.introduction, 'description': self.bigtuto.description, 'conclusion': self.bigtuto.conclusion, - 'licence' : self.bigtuto.licence.pk, - 'last_hash': compute_hash([os.path.join(self.bigtuto.get_path(),"introduction.md"), - os.path.join(self.bigtuto.get_path(),"conclusion.md")]) + 'licence': self.bigtuto.licence.pk, + 'last_hash': compute_hash([os.path.join(self.bigtuto.get_path(), "introduction.md"), + os.path.join(self.bigtuto.get_path(), "conclusion.md")]) }, follow=True) @@ -1937,14 +2093,14 @@ def test_workflow_licence(self): # check value first tuto = Tutorial.objects.get(pk=self.bigtuto.pk) self.assertEqual(tuto.licence.pk, self.licence.pk) - + # get value wich does not change (speed up test) introduction = self.bigtuto.get_introduction() conclusion = self.bigtuto.get_conclusion() hash = compute_hash([ - os.path.join(self.bigtuto.get_path(),"introduction.md"), - os.path.join(self.bigtuto.get_path(),"conclusion.md") - ]) + os.path.join(self.bigtuto.get_path(), "introduction.md"), + os.path.join(self.bigtuto.get_path(), "conclusion.md") + ]) # logout before self.client.logout() @@ -1957,15 +2113,15 @@ def test_workflow_licence(self): # change licence (get 302) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.bigtuto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.bigtuto.pk), { 'title': self.bigtuto.title, 'description': self.bigtuto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : new_licence.pk, + 'licence': new_licence.pk, 'last_hash': hash }, follow=False) @@ -1991,15 +2147,15 @@ def test_workflow_licence(self): # change licence back to old one (get 302, staff can change licence) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.bigtuto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.bigtuto.pk), { 'title': self.bigtuto.title, 'description': self.bigtuto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : self.licence.pk, + 'licence': self.licence.pk, 'last_hash': hash }, follow=False) @@ -2019,15 +2175,15 @@ def test_workflow_licence(self): # change licence (get 302, redirection to login page) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.bigtuto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.bigtuto.pk), { 'title': self.bigtuto.title, 'description': self.bigtuto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : new_licence.pk, + 'licence': new_licence.pk, 'last_hash': hash }, follow=False) @@ -2037,7 +2193,7 @@ def test_workflow_licence(self): tuto = Tutorial.objects.get(pk=self.bigtuto.pk) self.assertEqual(tuto.licence.pk, self.licence.pk) self.assertNotEqual(tuto.licence.pk, new_licence.pk) - + # login with random user self.assertTrue( self.client.login( @@ -2048,15 +2204,15 @@ def test_workflow_licence(self): # change licence (get 403, random user cannot edit bigtuto if not in # authors list) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.bigtuto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.bigtuto.pk), { 'title': self.bigtuto.title, 'description': self.bigtuto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : new_licence.pk, + 'licence': new_licence.pk, 'last_hash': hash }, follow=False) @@ -2071,6 +2227,175 @@ def test_workflow_licence(self): json = tuto.load_json() self.assertEquals(json['licence'], self.licence.code) + def test_workflow_archive_tuto(self): + """ensure the behavior of archive with a big tutorial""" + + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # modify tutorial, add a new extract (NOTE: zipfile does not ensure UTF-8): + extract_content = u'Le contenu de l\'extrait' + extract_title = u'Un titre d\'extrait' + result = self.client.post( + reverse('zds.tutorial.views.add_extract') + + '?chapitre={0}'.format( + self.chapter2_1.pk), + { + 'title': extract_title, + 'text': extract_content + }) + self.assertEqual(result.status_code, 302) + + # now, draft and public version are not the same + tutorial = Tutorial.objects.get(pk=self.bigtuto.pk) + self.assertNotEqual(tutorial.sha_draft, tutorial.sha_public) + # store extract + added_extract = Extract.objects.get(chapter=Chapter.objects.get(pk=self.chapter2_1.pk)) + + # fetch archives : + # 1. draft version + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + draft_zip_path = os.path.join(tempfile.gettempdir(), '__draft.zip') + f = open(draft_zip_path, 'w') + f.write(result.content) + f.close() + # 2. online version : + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + online_zip_path = os.path.join(tempfile.gettempdir(), '__online.zip') + f = open(online_zip_path, 'w') + f.write(result.content) + f.close() + + # note : path is "/2_ma-partie-no2/4_mon-chapitre-no4/" + # now check if modification are in draft version of archive and not in the public one + draft_zip = zipfile.ZipFile(draft_zip_path, 'r') + online_zip = zipfile.ZipFile(online_zip_path, 'r') + + # first, test in manifest + online_manifest = json_reader.loads(online_zip.read('manifest.json')) + found = False + for part in online_manifest['parts']: + if part['pk'] == self.part2.pk: + for chapter in part['chapters']: + if chapter['pk'] == self.chapter2_1.pk: + for extract in chapter['extracts']: + if extract['pk'] == added_extract.pk: + found = True + self.assertFalse(found) # extract cannot exists in the online version + + draft_manifest = json_reader.loads(draft_zip.read('manifest.json')) + extract_in_manifest = [] + for part in draft_manifest['parts']: + if part['pk'] == self.part2.pk: + for chapter in part['chapters']: + if chapter['pk'] == self.chapter2_1.pk: + for extract in chapter['extracts']: + if extract['pk'] == added_extract.pk: + found = True + extract_in_manifest = extract + self.assertTrue(found) # extract exists in draft version + self.assertEqual(extract_in_manifest['title'], extract_title) + + # and then, test the file directly : + found = True + try: + online_zip.getinfo(extract_in_manifest['text']) + except KeyError: + found = False + self.assertFalse(found) # extract cannot exists in the online version + + found = True + try: + draft_zip.getinfo(extract_in_manifest['text']) + except KeyError: + found = False + self.assertTrue(found) # extract exists in the draft one + self.assertEqual(draft_zip.read(extract_in_manifest['text']), extract_content) # content is good + + draft_zip.close() + online_zip.close() + + # then logout and test access + self.client.logout() + + # public cannot access to draft version of tutorial + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 403) + # ... but can access to online version + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + + # login with random user + self.assertEqual( + self.client.login( + username=self.user.username, + password='hostel77'), + True) + + # cannot access to draft version of tutorial (if not author or staff) + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 403) + # but can access to online one + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + self.client.logout() + + # login with staff user + self.assertEqual( + self.client.login( + username=self.staff.username, + password='hostel77'), + True) + + # staff can access to draft version of tutorial + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + # ... and also to online version + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.bigtuto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + + # finally, clean up things: + os.remove(draft_zip_path) + os.remove(online_zip_path) + def tearDown(self): if os.path.isdir(settings.REPO_PATH): shutil.rmtree(settings.REPO_PATH) @@ -2106,7 +2431,7 @@ def setUp(self): self.staff = StaffProfileFactory().user self.subcat = SubCategoryFactory() - + self.licence = LicenceFactory() self.licence.save() @@ -2164,27 +2489,97 @@ def setUp(self): mail.outbox = [] + def test_import_archive(self): + login_check = self.client.login( + username=self.user_author.username, + password='hostel77') + self.assertEqual(login_check, True) + # create temporary data directory + temp = os.path.join(tempfile.gettempdir(), "temp") + if not os.path.exists(temp): + os.makedirs(temp, mode=0777) + # download zip + repo_path = os.path.join(settings.REPO_PATH, self.minituto.get_phy_slug()) + repo = Repo(repo_path) + zip_path = os.path.join(tempfile.gettempdir(), self.minituto.slug + '.zip') + zip_file = zipfile.ZipFile(zip_path, 'w') + insert_into_zip(zip_file, repo.commit(self.minituto.sha_draft).tree) + zip_file.close() + + zip_dir = os.path.join(temp, self.minituto.get_phy_slug()) + if not os.path.exists(zip_dir): + os.makedirs(zip_dir, mode=0777) + + # Extract zip + with zipfile.ZipFile(zip_path) as zip_file: + for member in zip_file.namelist(): + filename = os.path.basename(member) + # skip directories + if not filename: + continue + if not os.path.exists(os.path.dirname(os.path.join(zip_dir, member))): + os.makedirs(os.path.dirname(os.path.join(zip_dir, member)), mode=0777) + # copy file (taken from zipfile's extract) + source = zip_file.open(member) + target = file(os.path.join(zip_dir, filename), "wb") + with source, target: + shutil.copyfileobj(source, target) + self.assertTrue(os.path.isdir(zip_dir)) + + # update markdown files + up_intro_tfile = open(os.path.join(temp, self.minituto.get_phy_slug(), self.minituto.introduction), "a") + up_intro_tfile.write(u"preuve de modification de l'introduction") + up_intro_tfile.close() + up_conclu_tfile = open(os.path.join(temp, self.minituto.get_phy_slug(), self.minituto.conclusion), "a") + up_conclu_tfile.write(u"preuve de modification de la conclusion") + up_conclu_tfile.close() + + # zip directory + shutil.make_archive(os.path.join(temp, self.minituto.get_phy_slug()), + "zip", + os.path.join(temp, self.minituto.get_phy_slug())) + + self.assertTrue(os.path.isfile(os.path.join(temp, self.minituto.get_phy_slug()+".zip"))) + # import zip archive + result = self.client.post( + reverse('zds.tutorial.views.import_tuto'), + { + 'file': open( + os.path.join( + temp, + os.path.join(temp, self.minituto.get_phy_slug()+".zip")), + 'r'), + 'tutorial': self.minituto.pk, + 'import-archive': "importer"}, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Tutorial.objects.all().count(), 1) + + # delete temporary data directory + shutil.rmtree(temp) + os.remove(zip_path) + def add_test_extract_named_introduction(self): """test the use of an extract named introduction""" self.client.login(username=self.user_author, - password='hostel77') + password='hostel77') result = self.client.post( - reverse( 'zds.tutorial.views.add_extract') + + reverse('zds.tutorial.views.add_extract') + '?chapitre={0}'.format( self.chapter.pk), { - 'title' : "Introduction", - 'text' : u"Le contenu de l'extrait" + 'title': "Introduction", + 'text': u"Le contenu de l'extrait" }) self.assertEqual(result.status_code, 302) tuto = Tutorial.objects.get(pk=self.minituto.pk) - self.assertEqual(Extract.objects.all().count(),1) - intro_path = os.path.join(tuto.get_path(),"introduction.md") - extract_path = Extract.objects.get(pk=1).get_path() - self.assertNotEqual(intro_path,extract_path) + self.assertEqual(Extract.objects.all().count(), 1) + intro_path = os.path.join(tuto.get_path(), "introduction.md") + extract_path = Extract.objects.get(pk=1).get_path() + self.assertNotEqual(intro_path, extract_path) self.assertTrue(os.path.isfile(intro_path)) self.assertTrue(os.path.isfile(extract_path)) @@ -2192,23 +2587,23 @@ def add_test_extract_named_conclusion(self): """test the use of an extract named introduction""" self.client.login(username=self.user_author, - password='hostel77') + password='hostel77') result = self.client.post( - reverse( 'zds.tutorial.views.add_extract') + + reverse('zds.tutorial.views.add_extract') + '?chapitre={0}'.format( self.chapter.pk), { - 'title' : "Conclusion", - 'text' : u"Le contenu de l'extrait" + 'title': "Conclusion", + 'text': u"Le contenu de l'extrait" }) self.assertEqual(result.status_code, 302) tuto = Tutorial.objects.get(pk=self.minituto.pk) - self.assertEqual(Extract.objects.all().count(),1) - ccl_path = os.path.join(tuto.get_path(),"conclusion.md") - extract_path = Extract.objects.get(pk=1).get_path() - self.assertNotEqual(ccl_path,extract_path) + self.assertEqual(Extract.objects.all().count(), 1) + ccl_path = os.path.join(tuto.get_path(), "conclusion.md") + extract_path = Extract.objects.get(pk=1).get_path() + self.assertNotEqual(ccl_path, extract_path) self.assertTrue(os.path.isfile(ccl_path)) self.assertTrue(os.path.isfile(extract_path)) @@ -2459,7 +2854,7 @@ def test_dislike_note(self): self.assertEqual(Note.objects.get(pk=note3.pk).dislike, 0) def test_import_tuto(self): - """Test import of big tuto.""" + """Test import of mini tuto.""" result = self.client.post( reverse('zds.tutorial.views.import_tuto'), { @@ -2468,17 +2863,18 @@ def test_import_tuto(self): settings.SITE_ROOT, 'fixtures', 'tuto', - 'temps-reel-avec-irrlicht', - 'temps-reel-avec-irrlicht.tuto'), + 'securisez-vos-mots-de-passe-avec-lastpass', + 'securisez-vos-mots-de-passe-avec-lastpass.tuto'), 'r'), 'images': open( os.path.join( settings.SITE_ROOT, 'fixtures', 'tuto', - 'temps-reel-avec-irrlicht', + 'securisez-vos-mots-de-passe-avec-lastpass', 'images.zip'), - 'r')}, + 'r'), + 'import-tuto': "importer"}, follow=False) self.assertEqual(result.status_code, 302) @@ -2711,36 +3107,36 @@ def test_delete_image_tutorial(self): # Attach an image of a gallery at a tutorial. image_tutorial = ImageFactory(gallery=self.minituto.gallery) - user_gallery = UserGalleryFactory(user=self.user_author, gallery=self.minituto.gallery) + UserGalleryFactory(user=self.user_author, gallery=self.minituto.gallery) self.minituto.image = image_tutorial self.minituto.save() - self.assertTrue(Tutorial.objects.get(pk=self.minituto.pk).image != None) + self.assertTrue(Tutorial.objects.get(pk=self.minituto.pk).image is not None) # Delete the image of the minituto. response = self.client.post( - reverse('zds.gallery.views.delete_image'), - { - 'gallery': self.minituto.gallery.pk, - 'delete_multi': '', - 'items': [image_tutorial.pk] - }, - follow=True + reverse('zds.gallery.views.delete_image'), + { + 'gallery': self.minituto.gallery.pk, + 'delete_multi': '', + 'items': [image_tutorial.pk] + }, + follow=True ) self.assertEqual(200, response.status_code) # Check if the tutorial is already in database and it doesn't have image. self.assertEqual(1, Tutorial.objects.filter(pk=self.minituto.pk).count()) - self.assertTrue(Tutorial.objects.get(pk=self.minituto.pk).image == None) + self.assertTrue(Tutorial.objects.get(pk=self.minituto.pk).image is None) def test_edit_tuto(self): "test that edition work well and avoid issue 1058" sub = SubCategory() sub.title = "toto" sub.save() - # logout before + # logout before self.client.logout() # first, login with author : self.assertEqual( @@ -2748,40 +3144,40 @@ def test_edit_tuto(self): username=self.user_author.username, password='hostel77'), True) - #edit the tuto without slug change + # edit the tuto without slug change response = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + "?tutoriel={0}".format(self.minituto.pk), - { - 'title': self.minituto.title, - 'description': "nouvelle description", - 'subcategory': [sub.pk], - 'introduction': self.minituto.get_introduction(), - 'conclusion': self.minituto.get_conclusion(), - 'licence' : self.minituto.licence.pk, - 'last_hash': compute_hash([os.path.join(self.minituto.get_path(),"introduction.md"), - os.path.join(self.minituto.get_path(),"conclusion.md")]) - }, - follow=False + reverse('zds.tutorial.views.edit_tutorial') + "?tutoriel={0}".format(self.minituto.pk), + { + 'title': self.minituto.title, + 'description': "nouvelle description", + 'subcategory': [sub.pk], + 'introduction': self.minituto.get_introduction(), + 'conclusion': self.minituto.get_conclusion(), + 'licence': self.minituto.licence.pk, + 'last_hash': compute_hash([os.path.join(self.minituto.get_path(), "introduction.md"), + os.path.join(self.minituto.get_path(), "conclusion.md")]) + }, + follow=False ) self.assertEqual(302, response.status_code) tuto = Tutorial.objects.filter(pk=self.minituto.pk).first() self.assertEqual(tuto.title, self.minituto.title) self.assertEqual(tuto.description, "nouvelle description") - #edit tuto with a slug change + # edit tuto with a slug change (introduction, conclusion) = (self.minituto.get_introduction(), self.minituto.get_conclusion()) self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + "?tutoriel={0}".format(self.minituto.pk), - { - 'title': "nouveau titre pour nouveau slug", - 'description': "nouvelle description", - 'subcategory': [sub.pk], - 'introduction': self.minituto.get_introduction(), - 'conclusion': self.minituto.get_conclusion(), - 'licence' : self.minituto.licence.pk, - 'last_hash': compute_hash([os.path.join(self.minituto.get_path(),"introduction.md"), - os.path.join(self.minituto.get_path(),"conclusion.md")]) - }, - follow=False + reverse('zds.tutorial.views.edit_tutorial') + "?tutoriel={0}".format(self.minituto.pk), + { + 'title': "nouveau titre pour nouveau slug", + 'description': "nouvelle description", + 'subcategory': [sub.pk], + 'introduction': introduction, + 'conclusion': conclusion, + 'licence': self.minituto.licence.pk, + 'last_hash': compute_hash([os.path.join(self.minituto.get_path(), "introduction.md"), + os.path.join(self.minituto.get_path(), "conclusion.md")]) + }, + follow=False ) tuto = Tutorial.objects.filter(pk=self.minituto.pk).first() self.assertEqual(tuto.title, "nouveau titre pour nouveau slug") @@ -2790,7 +3186,7 @@ def test_edit_tuto(self): def test_reorder_tuto(self): "test that reordering makes it good to avoid #1060" - # logout before + # logout before self.client.logout() # first, login with author : self.assertEqual( @@ -2798,54 +3194,58 @@ def test_reorder_tuto(self): username=self.user_author.username, password='hostel77'), True) - (introduction, conclusion) = (self.minituto.get_introduction(), self.minituto.get_conclusion()) + introduction = self.minituto.get_introduction() # prepare the extracts self.client.post( - reverse('zds.tutorial.views.add_extract') + "?chapitre={0}".format(self.minituto.get_chapter().pk), - { - - 'title': "extract 1", - 'text': "extract 1 text" - }, - follow=False + reverse('zds.tutorial.views.add_extract') + "?chapitre={0}".format(self.minituto.get_chapter().pk), + { + + 'title': "extract 1", + 'text': "extract 1 text" + }, + follow=False ) extract_pk = Tutorial.objects.get(pk=self.minituto.pk).get_chapter().get_extracts()[0].pk self.client.post( - reverse('zds.tutorial.views.add_extract') + "?chapitre={0}".format(self.minituto.get_chapter().pk), - { - - 'title': "extract 2", - 'text': "extract 2 text" - }, - follow=False + reverse('zds.tutorial.views.add_extract') + "?chapitre={0}".format(self.minituto.get_chapter().pk), + { + + 'title': "extract 2", + 'text': "extract 2 text" + }, + follow=False ) self.assertEqual(2, self.minituto.get_chapter().get_extracts().count()) # reorder self.client.post( - reverse('zds.tutorial.views.modify_extract'), - { - 'move': "", - 'move_target': 2, - 'extract': self.minituto.get_chapter().get_extracts()[0].pk - }, - follow=False + reverse('zds.tutorial.views.modify_extract'), + { + 'move': "", + 'move_target': 2, + 'extract': self.minituto.get_chapter().get_extracts()[0].pk + }, + follow=False ) # this test check issue 1060 self.assertEqual(introduction, Tutorial.objects.filter(pk=self.minituto.pk).first().get_introduction()) self.assertEqual(2, Extract.objects.get(pk=extract_pk).position_in_chapter) - - def test_workflow_beta_tuto(self) : + + def test_workflow_beta_tuto(self): "Ensure the behavior of the beta version of tutorials" + ForumFactory( + category=CategoryFactory(position=1), + position_in_category=1) + # logout before self.client.logout() # check if acess to page with beta tutorial (with guest) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) self.assertEqual(200, response.status_code) @@ -2858,27 +3258,27 @@ def test_workflow_beta_tuto(self) : # then active the beta on tutorial : sha_draft = Tutorial.objects.get(pk=self.minituto.pk).sha_draft response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.minituto.pk, - 'activ_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.minituto.pk, + 'activ_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) # check if acess to page with beta tutorial (with author) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) self.assertEqual(200, response.status_code) # test beta : self.assertEqual( - Tutorial.objects.get(pk=self.minituto.pk).sha_beta, + Tutorial.objects.get(pk=self.minituto.pk).sha_beta, sha_draft) url = Tutorial.objects.get(pk=self.minituto.pk).get_absolute_url_beta() sha_beta = sha_draft @@ -2903,13 +3303,13 @@ def test_workflow_beta_tuto(self) : # check if acess to page with beta tutorial (with random user) response = self.client.get( - reverse( - 'zds.tutorial.views.find_tuto', - args=[self.user_author.pk] - ) + '?type=beta' + reverse( + 'zds.tutorial.views.find_tuto', + args=[self.user_author.pk] + ) + '?type=beta' ) self.assertEqual(200, response.status_code) - + # then modify tutorial self.assertEqual( self.client.login( @@ -2917,31 +3317,31 @@ def test_workflow_beta_tuto(self) : password='hostel77'), True) result = self.client.post( - reverse( 'zds.tutorial.views.add_extract') + + reverse('zds.tutorial.views.add_extract') + '?chapitre={0}'.format( self.chapter.pk), { - 'title' : "Introduction", - 'text' : u"Le contenu de l'extrait" + 'title': "Introduction", + 'text': u"Le contenu de l'extrait" }) self.assertEqual(result.status_code, 302) self.assertEqual( - Tutorial.objects.get(pk=self.minituto.pk).sha_beta, + Tutorial.objects.get(pk=self.minituto.pk).sha_beta, sha_beta) self.assertNotEqual( - Tutorial.objects.get(pk=self.minituto.pk).sha_draft, + Tutorial.objects.get(pk=self.minituto.pk).sha_draft, sha_beta) # update beta sha_draft = Tutorial.objects.get(pk=self.minituto.pk).sha_draft response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.minituto.pk, - 'update_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.minituto.pk, + 'update_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) old_url = url @@ -2970,17 +3370,17 @@ def test_workflow_beta_tuto(self) : password='hostel77'), True) response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.minituto.pk, - 'desactiv_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.minituto.pk, + 'desactiv_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) self.assertEqual( - Tutorial.objects.get(pk=self.minituto.pk).sha_beta, + Tutorial.objects.get(pk=self.minituto.pk).sha_beta, None) # test access from outside (get 302, connexion form) self.client.logout() @@ -3004,13 +3404,13 @@ def test_workflow_beta_tuto(self) : password='hostel77'), True) response = self.client.post( - reverse('zds.tutorial.views.modify_tutorial'), - { - 'tutorial': self.minituto.pk, - 'activ_beta': True, - 'version': sha_draft - }, - follow=False + reverse('zds.tutorial.views.modify_tutorial'), + { + 'tutorial': self.minituto.pk, + 'activ_beta': True, + 'version': sha_draft + }, + follow=False ) self.assertEqual(302, response.status_code) # test access from outside (get 302, connexion form) @@ -3031,6 +3431,10 @@ def test_workflow_beta_tuto(self) : def test_workflow_tuto(self): """Test workflow of mini tutorial.""" + ForumFactory( + category=CategoryFactory(position=1), + position_in_category=1) + # logout before self.client.logout() # login with simple member @@ -3039,27 +3443,27 @@ def test_workflow_tuto(self): username=self.user.username, password='hostel77'), True) - - #add new mini tuto + + # add new mini tuto result = self.client.post( reverse('zds.tutorial.views.add_tutorial'), { 'title': u"Introduction à l'algèbre", 'description': "Perçer les mystère de boole", - 'introduction':"Bienvenue dans le monde binaires", + 'introduction': "Bienvenue dans le monde binaires", 'conclusion': "", 'type': "MINI", - 'licence' : self.licence.pk, + 'licence': self.licence.pk, 'subcategory': self.subcat.pk, }, follow=False) - + self.assertEqual(result.status_code, 302) self.assertEqual(Tutorial.objects.all().count(), 2) tuto = Tutorial.objects.last() chapter = Chapter.objects.filter(tutorial__pk=tuto.pk).first() - - #add extract 1 + + # add extract 1 result = self.client.post( reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(chapter.pk), { @@ -3071,7 +3475,7 @@ def test_workflow_tuto(self): self.assertEqual(Extract.objects.filter(chapter=chapter).count(), 1) e1 = Extract.objects.filter(chapter=chapter).last() - #check view offline + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_tutorial', @@ -3079,10 +3483,10 @@ def test_workflow_tuto(self): tuto.pk, tuto.slug]), follow=True) - self.assertContains(response=result, text = u"Extrait 1") - self.assertContains(response=result, text = u"Introduisons notre premier extrait") - - #add extract 2 + self.assertContains(response=result, text=u"Extrait 1") + self.assertContains(response=result, text=u"Introduisons notre premier extrait") + + # add extract 2 result = self.client.post( reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(chapter.pk), { @@ -3094,7 +3498,7 @@ def test_workflow_tuto(self): self.assertEqual(Extract.objects.filter(chapter=chapter).count(), 2) e2 = Extract.objects.filter(chapter=chapter).last() - #check view offline + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_tutorial', @@ -3102,10 +3506,10 @@ def test_workflow_tuto(self): tuto.pk, tuto.slug]), follow=True) - self.assertContains(response=result, text = u"Extrait 2") - self.assertContains(response=result, text = u"Introduisons notre deuxième extrait") + self.assertContains(response=result, text=u"Extrait 2") + self.assertContains(response=result, text=u"Introduisons notre deuxième extrait") - #add extract 3 + # add extract 3 result = self.client.post( reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(chapter.pk), { @@ -3115,9 +3519,9 @@ def test_workflow_tuto(self): follow=False) self.assertEqual(result.status_code, 302) self.assertEqual(Extract.objects.filter(chapter=chapter).count(), 3) - e3 = Extract.objects.filter(chapter=chapter).last() + Extract.objects.filter(chapter=chapter).last() - #check view offline + # check view offline result = self.client.get( reverse( 'zds.tutorial.views.view_tutorial', @@ -3125,10 +3529,10 @@ def test_workflow_tuto(self): tuto.pk, tuto.slug]), follow=True) - self.assertContains(response=result, text = u"Extrait 3") - self.assertContains(response=result, text = u"Introduisons notre troisième extrait") - - #edit extract 2 + self.assertContains(response=result, text=u"Extrait 3") + self.assertContains(response=result, text=u"Introduisons notre troisième extrait") + + # edit extract 2 result = self.client.post( reverse('zds.tutorial.views.edit_extract') + '?extrait={}'.format(e2.pk), { @@ -3138,11 +3542,11 @@ def test_workflow_tuto(self): }, follow=True) self.assertEqual(result.status_code, 200) - self.assertContains(response=result, text = u"Extrait 2 : edition de titre") - self.assertContains(response=result, text = u"Edition d'introduction") + self.assertContains(response=result, text=u"Extrait 2 : edition de titre") + self.assertContains(response=result, text=u"Edition d'introduction") self.assertEqual(Extract.objects.filter(chapter__tutorial=tuto).count(), 3) - - #move extract 1 against 2 + + # move extract 1 against 2 result = self.client.post( reverse('zds.tutorial.views.modify_extract'), { @@ -3151,7 +3555,7 @@ def test_workflow_tuto(self): }, follow=True) - #delete extract 1 + # delete extract 1 result = self.client.post( reverse('zds.tutorial.views.modify_extract'), { @@ -3172,14 +3576,14 @@ def test_workflow_licence(self): # check value first tuto = Tutorial.objects.get(pk=self.minituto.pk) self.assertEqual(tuto.licence.pk, self.licence.pk) - + # get value wich does not change (speed up test) introduction = self.minituto.get_introduction() conclusion = self.minituto.get_conclusion() hash = compute_hash([ - os.path.join(self.minituto.get_path(),"introduction.md"), - os.path.join(self.minituto.get_path(),"conclusion.md") - ]) + os.path.join(self.minituto.get_path(), "introduction.md"), + os.path.join(self.minituto.get_path(), "conclusion.md") + ]) # logout before self.client.logout() @@ -3192,15 +3596,15 @@ def test_workflow_licence(self): # change licence (get 302) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.minituto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.minituto.pk), { 'title': self.minituto.title, 'description': self.minituto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : new_licence.pk, + 'licence': new_licence.pk, 'last_hash': hash }, follow=False) @@ -3226,15 +3630,15 @@ def test_workflow_licence(self): # change licence back to old one (get 302, staff can change licence) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.minituto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.minituto.pk), { 'title': self.minituto.title, 'description': self.minituto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : self.licence.pk, + 'licence': self.licence.pk, 'last_hash': hash }, follow=False) @@ -3254,15 +3658,15 @@ def test_workflow_licence(self): # change licence (get 302, redirection to login page) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.minituto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.minituto.pk), { 'title': self.minituto.title, 'description': self.minituto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : new_licence.pk, + 'licence': new_licence.pk, 'last_hash': hash }, follow=False) @@ -3272,7 +3676,7 @@ def test_workflow_licence(self): tuto = Tutorial.objects.get(pk=self.minituto.pk) self.assertEqual(tuto.licence.pk, self.licence.pk) self.assertNotEqual(tuto.licence.pk, new_licence.pk) - + # login with random user self.assertTrue( self.client.login( @@ -3283,15 +3687,15 @@ def test_workflow_licence(self): # change licence (get 403, random user cannot edit minituto if not in # authors list) : result = self.client.post( - reverse('zds.tutorial.views.edit_tutorial') + - '?tutoriel={}'.format(self.minituto.pk), + reverse('zds.tutorial.views.edit_tutorial') + + '?tutoriel={}'.format(self.minituto.pk), { 'title': self.minituto.title, 'description': self.minituto.description, 'introduction': introduction, 'conclusion': conclusion, 'subcategory': self.subcat.pk, - 'licence' : new_licence.pk, + 'licence': new_licence.pk, 'last_hash': hash }, follow=False) @@ -3306,6 +3710,166 @@ def test_workflow_licence(self): json = tuto.load_json() self.assertEquals(json['licence'], self.licence.code) + def test_workflow_archive_tuto(self): + """ensure the behavior of archive with a mini tutorial""" + + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # modify tutorial, add a new extract (NOTE: zipfile does not ensure UTF-8) : + extract_title = u'Un titre d\'extrait' + extract_content = u'To be or not to be, that\'s the question (extract of Hamlet)' + result = self.client.post( + reverse('zds.tutorial.views.add_extract') + + '?chapitre={0}'.format( + self.chapter.pk), + { + 'title': extract_title, + 'text': extract_content + }) + self.assertEqual(result.status_code, 302) + + # now, draft and public version are not the same + tutorial = Tutorial.objects.get(pk=self.minituto.pk) + self.assertNotEqual(tutorial.sha_draft, tutorial.sha_public) + # store extract + added_extract = Extract.objects.get(chapter=Chapter.objects.get(pk=self.chapter.pk)) + + # fetch archives : + # 1. draft version + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + draft_zip_path = os.path.join(tempfile.gettempdir(), '__draft.zip') + f = open(draft_zip_path, 'w') + f.write(result.content) + f.close() + # 2. online version : + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + online_zip_path = os.path.join(tempfile.gettempdir(), '__online.zip') + f = open(online_zip_path, 'w') + f.write(result.content) + f.close() + + # now check if modification are in draft version of archive and not in the public one + draft_zip = zipfile.ZipFile(draft_zip_path, 'r') + online_zip = zipfile.ZipFile(online_zip_path, 'r') + + # first, test in manifest + online_manifest = json_reader.loads(online_zip.read('manifest.json')) + found = False + for extract in online_manifest['chapter']['extracts']: + if extract['pk'] == added_extract.pk: + found = True + self.assertFalse(found) # extract cannot exists in the online version + + draft_manifest = json_reader.loads(draft_zip.read('manifest.json')) + extract_in_manifest = [] + for extract in draft_manifest['chapter']['extracts']: + if extract['pk'] == added_extract.pk: + found = True + extract_in_manifest = extract + self.assertTrue(found) # extract exists in draft version + self.assertEqual(extract_in_manifest['title'], extract_title) + + # and then, test the file directly : + found = True + try: + online_zip.getinfo(extract_in_manifest['text']) + except KeyError: + found = False + self.assertFalse(found) # extract cannot exists in the online version + + found = True + try: + draft_zip.getinfo(extract_in_manifest['text']) + except KeyError: + found = False + self.assertTrue(found) # extract exists in the draft one + self.assertEqual(draft_zip.read(extract_in_manifest['text']), extract_content) # content is good + + draft_zip.close() + online_zip.close() + + # then logout and test access + self.client.logout() + + # public cannot access to draft version of tutorial + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 403) + # ... but can access to online version + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + + # login with random user + self.assertEqual( + self.client.login( + username=self.user.username, + password='hostel77'), + True) + + # cannot access to draft version of tutorial (if not author or staff) + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 403) + # but can access to online one + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + self.client.logout() + + # login with staff user + self.assertEqual( + self.client.login( + username=self.staff.username, + password='hostel77'), + True) + + # staff can access to draft version of tutorial + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + # ... and also to online version + result = self.client.get( + reverse('zds.tutorial.views.download') + + '?tutoriel={0}&online'.format( + self.minituto.pk), + follow=False) + self.assertEqual(result.status_code, 200) + + # finally, clean up things: + os.remove(draft_zip_path) + os.remove(online_zip_path) + def tearDown(self): if os.path.isdir(settings.REPO_PATH): shutil.rmtree(settings.REPO_PATH) diff --git a/zds/tutorial/urls.py b/zds/tutorial/urls.py index 049e4ebd68..c1b6736880 100644 --- a/zds/tutorial/urls.py +++ b/zds/tutorial/urls.py @@ -3,10 +3,12 @@ from django.conf.urls import patterns, url from . import views - +from . import feeds urlpatterns = patterns('', # Viewing + url(r'^flux/rss/$', feeds.LastTutorialsFeedRSS(), name='tutorial-feed-rss'), + url(r'^flux/atom/$', feeds.LastTutorialsFeedATOM(), name='tutorial-feed-atom'), # Current URLs url(r'^recherche/(?P\d+)/$', diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py index fc0b25f03d..9941d3b6ee 100644 --- a/zds/tutorial/views.py +++ b/zds/tutorial/views.py @@ -4,6 +4,7 @@ from datetime import datetime from operator import attrgetter from urllib import urlretrieve +from django.contrib.humanize.templatetags.humanize import naturaltime from urlparse import urlparse, parse_qs try: import ujson as json_reader @@ -14,10 +15,11 @@ import json as json_reader import json as json_writer -import os.path -import re import shutil +import re import zipfile +import os +import tempfile from PIL import Image as ImagePIL from django.conf import settings @@ -34,28 +36,31 @@ from django.shortcuts import get_object_or_404, redirect from django.utils.encoding import smart_str from django.views.decorators.http import require_POST -from git import * +from git import Repo, Actor from lxml import etree from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ - ExtractForm, ImportForm, NoteForm, AskValidationForm, ValidForm, RejectForm + ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ mark_read, Note from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip +from zds.forum.models import Forum, Topic from zds.utils import render_template from zds.utils import slugify from zds.utils.models import Alert from zds.utils.models import Category, Licence, CommentLike, CommentDislike, \ SubCategory from zds.utils.mps import send_mp +from zds.utils.forums import create_topic, send_post, lock_topic, unlock_topic from zds.utils.paginator import paginator_range from zds.utils.templatetags.emarkdown import emarkdown -from zds.utils.tutorials import get_blob, export_tutorial_to_md, move +from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, import_archive from zds.utils.misc import compute_hash, content_has_changed + def render_chapter_form(chapter): if chapter.part: return ChapterForm({"title": chapter.title, @@ -67,6 +72,7 @@ def render_chapter_form(chapter): EmbdedChapterForm({"introduction": chapter.get_introduction(), "conclusion": chapter.get_conclusion()}) + def index(request): """Display all public tutorials of the website.""" @@ -89,7 +95,7 @@ def index(request): tutorials = Tutorial.objects.filter( sha_public__isnull=False, subcategory__in=[tag]).exclude(sha_public="").order_by("-pubdate").all() - + tuto_versions = [] for tutorial in tutorials: mandata = tutorial.load_json_for_public() @@ -166,7 +172,6 @@ def list_validation(request): {"validations": validations}) - @permission_required("tutorial.change_tutorial", raise_exception=True) @login_required @require_POST @@ -195,7 +200,6 @@ def reservation(request, validation_pk): ) - @login_required def diff(request, tutorial_pk, tutorial_slug): try: @@ -218,7 +222,6 @@ def diff(request, tutorial_pk, tutorial_slug): }) - @login_required def history(request, tutorial_pk, tutorial_slug): """History of the tutorial.""" @@ -235,7 +238,6 @@ def history(request, tutorial_pk, tutorial_slug): {"tutorial": tutorial, "logs": logs}) - @login_required @permission_required("tutorial.change_tutorial", raise_exception=True) def history_validation(request, tutorial_pk): @@ -307,11 +309,11 @@ def reject_tutorial(request): u'N\'hésite pas à lui envoyer un petit message pour discuter ' u'de la décision ou demander plus de détails si tout cela te ' u'semble injuste ou manque de clarté.'.format( - author.username, - tutorial.title, - validation.validator.username, - settings.SITE_URL + validation.validator.profile.get_absolute_url(), - validation.comment_validator)) + author.username, + tutorial.title, + validation.validator.username, + settings.SITE_URL + validation.validator.profile.get_absolute_url(), + validation.comment_validator)) bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) send_mp( bot, @@ -326,8 +328,8 @@ def reject_tutorial(request): + validation.version) else: messages.error(request, - "Vous devez avoir réservé ce tutoriel " - "pour pouvoir le refuser.") + "Vous devez avoir réservé ce tutoriel " + "pour pouvoir le refuser.") return redirect(tutorial.get_absolute_url() + "?version=" + validation.version) @@ -351,7 +353,7 @@ def valid_tutorial(request): version=tutorial.sha_validation).latest("date_proposition") if request.user == validation.validator: - (output, err) = MEP(tutorial, tutorial.sha_validation) + (output, err) = mep(tutorial, tutorial.sha_validation) messages.info(request, output) messages.error(request, err) validation.comment_validator = request.POST["text"] @@ -381,11 +383,11 @@ def valid_tutorial(request): u'd\'apporter des corrections/compléments.' u'Un Tutoriel vivant et à jour est bien plus lu ' u'qu\'un sujet abandonné !'.format( - author.username, - tutorial.title, - settings.SITE_URL + tutorial.get_absolute_url_online(), - validation.validator.username, - settings.SITE_URL + validation.validator.profile.get_absolute_url())) + author.username, + tutorial.title, + settings.SITE_URL + tutorial.get_absolute_url_online(), + validation.validator.username, + settings.SITE_URL + validation.validator.profile.get_absolute_url())) bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) send_mp( bot, @@ -400,8 +402,8 @@ def valid_tutorial(request): + validation.version) else: messages.error(request, - "Vous devez avoir réservé ce tutoriel " - "pour pouvoir le valider.") + "Vous devez avoir réservé ce tutoriel " + "pour pouvoir le valider.") return redirect(tutorial.get_absolute_url() + "?version=" + validation.version) @@ -416,7 +418,7 @@ def invalid_tutorial(request, tutorial_pk): # Retrieve current tutorial tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - UNMEP(tutorial) + un_mep(tutorial) validation = Validation.objects.filter( tutorial__pk=tutorial_pk, version=tutorial.sha_public).latest("date_proposition") @@ -459,20 +461,43 @@ def ask_validation(request): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - #delete old pending validation + old_validation = Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING_V']).first() + if old_validation is not None: + old_validator = old_validation.validator + else: + old_validator = None + # delete old pending validation Validation.objects.filter(tutorial__pk=tutorial_pk, - status__in=['PENDING','PENDING_V'])\ - .delete() + status__in=['PENDING', 'PENDING_V'])\ + .delete() # We create and save validation object of the tutorial. - - + validation = Validation() validation.tutorial = tutorial validation.date_proposition = datetime.now() validation.comment_authors = request.POST["text"] validation.version = request.POST["version"] + if old_validator is not None: + validation.validator = old_validator + validation.date_reserve + bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) + msg = \ + (u'Bonjour {0},' + u'Le tutoriel *{1}* que tu as réservé a été mis à jour en zone de validation, ' + u'Pour retrouver les modifications qui ont été faites, je t\'invite à ' + u'consulter l\'historique des versions' + u'\n\n> Merci'.format(old_validator.username, tutorial.title)) + send_mp( + bot, + [old_validator], + u"Mise à jour de tuto : {0}".format(tutorial.title), + "En validation", + msg, + False, + ) validation.save() - validation.tutorial.source=request.POST["source"] + validation.tutorial.source = request.POST["source"] validation.tutorial.sha_validation = request.POST["version"] validation.tutorial.save() messages.success(request, @@ -544,7 +569,6 @@ def modify_tutorial(request): raise Http404 tutorial_pk = request.POST["tutorial"] tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - # User actions if request.user in tutorial.authors.all() or request.user.has_perm("tutorial.change_tutorial"): @@ -572,6 +596,31 @@ def modify_tutorial(request): messages.success(request, u'L\'auteur {0} a bien été ajouté à la rédaction ' u'du tutoriel.'.format(author.username)) + + # send msg to new author + + msg = ( + u'Bonjour **{0}**,\n\n' + u'Tu as été ajouté comme auteur du tutoriel [{1}]({2}).\n' + u'Tu peux retrouver ce tutoriel en [cliquant ici]({3}), ou *via* le lien "En rédaction" du menu ' + u'"Tutoriels" sur la page de ton profil.\n\n' + u'Tu peux maintenant commencer à rédiger !'.format( + author.username, + tutorial.title, + settings.SITE_URL + tutorial.get_absolute_url(), + settings.SITE_URL + reverse("zds.member.views.tutorials")) + ) + bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) + send_mp( + bot, + [author], + u"Ajout en tant qu'auteur : {0}".format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(redirect_url) elif "remove_author" in request.POST: redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ @@ -599,11 +648,85 @@ def modify_tutorial(request): messages.success(request, u"L'auteur {0} a bien été retiré du tutoriel." .format(author.username)) + + # send msg to removed author + + msg = ( + u'Bonjour **{0}**,\n\n' + u'Tu as été supprimé des auteurs du tutoriel [{1}]({2}). Tant qu\'il ne sera pas publié, tu ne ' + u'pourra plus y accéder.\n'.format( + author.username, + tutorial.title, + settings.SITE_URL + tutorial.get_absolute_url()) + ) + bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) + send_mp( + bot, + [author], + u"Suppression des auteurs : {0}".format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(redirect_url) elif "activ_beta" in request.POST: if "version" in request.POST: tutorial.sha_beta = request.POST['version'] tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.BETA_FORUM_ID).first() + msg = \ + (u'Bonjour à tous,\n\n' + u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' + u'J\'aimerai obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' + u'sur la forme, afin de proposer en validation un texte de qualité.' + u'\n\nSi vous êtes interessé, cliquez ci-dessous ' + u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' + u'\n\nMerci d\'avance pour votre aide'.format( + naturaltime(tutorial.create_at), + tutorial.title, + settings.SITE_URL + tutorial.get_absolute_url_beta())) + if topic is None: + forum = get_object_or_404(Forum, pk=settings.BETA_FORUM_ID) + + create_topic(author=request.user, + forum=forum, + title=u"[beta][tutoriel]{0}".format(tutorial.title), + subtitle=u"{}".format(tutorial.description), + text=msg, + key=tutorial.pk + ) + tp = Topic.objects.get(key=tutorial.pk) + bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) + private_mp = \ + (u'Bonjour {},\n\n' + u'Vous venez de mettre votre tutoriel **{}** en beta. La communauté ' + u'pourra le consulter afin de vous faire des retours ' + u'constructifs avant sa soumission en validation.\n\n' + u'Un sujet dédié pour la beta de votre tutoriel a été ' + u'crée dans le forum et est accessible [ici]({})'.format( + request.user.username, + tutorial.title, + settings.SITE_URL + tp.get_absolute_url())) + send_mp( + bot, + [request.user], + u"Tutoriel en beta : {0}".format(tutorial.title), + "", + private_mp, + False, + ) + else: + msg_up = \ + (u'Bonjour,\n\n' + u'La beta du tutoriel est de nouveau active.' + u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' + u'\n\nMerci pour vos relectures'.format(tutorial.title, + settings.SITE_URL + tutorial.get_absolute_url_beta())) + unlock_topic(topic, msg) + send_post(topic, msg_up) + messages.success(request, u"La BETA sur ce tutoriel est bien activée.") else: messages.error(request, u"Impossible d'activer la BETA sur ce tutoriel.") @@ -612,6 +735,37 @@ def modify_tutorial(request): if "version" in request.POST: tutorial.sha_beta = request.POST['version'] tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.BETA_FORUM_ID).first() + msg = \ + (u'Bonjour à tous,\n\n' + u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' + u'J\'aimerai obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' + u'sur la forme, afin de proposer en validation un texte de qualité.' + u'\n\nSi vous êtes interessé, cliquez ci-dessous ' + u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' + u'\n\nMerci d\'avance pour votre aide'.format( + naturaltime(tutorial.create_at), + tutorial.title, + settings.SITE_URL + tutorial.get_absolute_url_beta())) + if topic is None: + forum = get_object_or_404(Forum, pk=settings.BETA_FORUM_ID) + + create_topic(author=request.user, + forum=forum, + title=u"[beta][tutoriel]{0}".format(tutorial.title), + subtitle=u"{}".format(tutorial.description), + text=msg, + key=tutorial.pk + ) + else: + msg_up = \ + (u'Bonjour, !\n\n' + u'La beta du tutoriel a été mise à jour.' + u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' + u'\n\nMerci pour vos relectures'.format(tutorial.title, + settings.SITE_URL + tutorial.get_absolute_url_beta())) + unlock_topic(topic, msg) + send_post(topic, msg_up) messages.success(request, u"La BETA sur ce tutoriel a bien été mise à jour.") else: messages.error(request, u"Impossible de mettre à jour la BETA sur ce tutoriel.") @@ -619,6 +773,13 @@ def modify_tutorial(request): elif "desactiv_beta" in request.POST: tutorial.sha_beta = None tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.BETA_FORUM_ID).first() + if topic is not None: + msg = \ + (u'Désactivation de la beta du tutoriel **{}**' + u'\n\nPour plus d\'informations envoyez moi un MP'.format(tutorial.title)) + lock_topic(topic) + send_post(topic, msg) messages.info(request, u"La BETA sur ce tutoriel a été désactivée.") return redirect(tutorial.get_absolute_url()) @@ -643,7 +804,7 @@ def view_tutorial(request, tutorial_pk, tutorial_slug): sha = request.GET["version"] except KeyError: sha = tutorial.sha_draft - + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() # Only authors of the tutorial and staff can view tutorial in offline. @@ -652,7 +813,6 @@ def view_tutorial(request, tutorial_pk, tutorial_slug): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # Two variables to handle two distinct cases (large/small tutorial) chapter = None @@ -668,8 +828,6 @@ def view_tutorial(request, tutorial_pk, tutorial_slug): mandata = json_reader.loads(manifest) tutorial.load_dic(mandata, sha) tutorial.load_introduction_and_conclusion(mandata, sha) - - #print mandata # If it's a small tutorial, fetch its chapter @@ -720,27 +878,27 @@ def view_tutorial(request, tutorial_pk, tutorial_slug): cpt_c += 1 cpt_p += 1 validation = Validation.objects.filter(tutorial__pk=tutorial.pk)\ - .order_by("-date_proposition")\ - .first() - formAskValidation = AskValidationForm() + .order_by("-date_proposition")\ + .first() if tutorial.source: - formValid = ValidForm(initial={"source": tutorial.source}) + form_ask_validation = AskValidationForm(initial={"source": tutorial.source}) + form_valid = ValidForm(initial={"source": tutorial.source}) else: - formValid = ValidForm() - formReject = RejectForm() + form_ask_validation = AskValidationForm() + form_valid = ValidForm() + form_reject = RejectForm() return render_template("tutorial/tutorial/view.html", { "tutorial": mandata, "chapter": chapter, "parts": parts, "version": sha, "validation": validation, - "formAskValidation": formAskValidation, - "formValid": formValid, - "formReject": formReject, + "formAskValidation": form_ask_validation, + "formValid": form_valid, + "formReject": form_reject, }) - def view_tutorial_online(request, tutorial_pk, tutorial_slug): """Display a tutorial.""" @@ -971,8 +1129,8 @@ def add_tutorial(request): else: form = TutorialForm( initial={ - 'licence' : Licence.objects.get(pk=settings.DEFAULT_LICENCE_PK) - } + 'licence': Licence.objects.get(pk=settings.DEFAULT_LICENCE_PK) + } ) return render_template("tutorial/tutorial/new.html", {"form": form}) @@ -1014,11 +1172,11 @@ def edit_tutorial(request): }) return render_template("tutorial/tutorial/edit.html", - { - "tutorial": tutorial, "form": form, - "last_hash": compute_hash([introduction, conclusion]), - "new_version": True - }) + { + "tutorial": tutorial, "form": form, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True + }) old_slug = tutorial.get_path() tutorial.title = data["title"] tutorial.description = data["description"] @@ -1056,7 +1214,7 @@ def edit_tutorial(request): tutorial.update_children() new_slug = os.path.join(settings.REPO_PATH, tutorial.get_phy_slug()) - + maj_repo_tuto( request, old_slug_path=old_slug, @@ -1077,7 +1235,7 @@ def edit_tutorial(request): licence = Licence.objects.filter(code=json["licence"]).all()[0] else: licence = Licence.objects.get( - pk=settings.DEFAULT_LICENCE_PK + pk=settings.DEFAULT_LICENCE_PK ) form = TutorialForm(initial={ "title": json["title"], @@ -1094,7 +1252,6 @@ def edit_tutorial(request): # Parts. - @login_required def view_part( request, @@ -1160,18 +1317,17 @@ def view_part( final_part = part break cpt_p += 1 - + # if part can't find if not find: raise Http404 - + return render_template("tutorial/part/view.html", {"tutorial": mandata, "part": final_part, "version": sha}) - def view_part_online( request, tutorial_pk, @@ -1194,7 +1350,7 @@ def view_part_online( mandata["get_parts"] = mandata["parts"] parts = mandata["parts"] cpt_p = 1 - final_part= None + final_part = None find = False for part in parts: part["tutorial"] = mandata @@ -1211,7 +1367,7 @@ def view_part_online( part["conclusion"] + ".html"), "r") part["conclu"] = conclu.read() conclu.close() - final_part=part + final_part = part cpt_c = 1 for chapter in part["chapters"]: chapter["part"] = part @@ -1234,7 +1390,7 @@ def view_part_online( # if part can't find if not find: raise Http404 - + return render_template("tutorial/part/view_online.html", {"part": final_part}) @@ -1270,7 +1426,7 @@ def add_part(request): part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") part.save() - + new_slug = os.path.join(settings.REPO_PATH, part.tutorial.get_phy_slug(), part.get_phy_slug()) maj_repo_part( @@ -1317,14 +1473,14 @@ def modify_part(request): move(part, new_pos, "position_in_tutorial", "tutorial", "get_parts") part.save() - + new_slug_path = os.path.join(settings.REPO_PATH, part.tutorial.get_phy_slug()) - + maj_repo_tuto(request, - old_slug_path = new_slug_path, - new_slug_path = new_slug_path, - tuto = part.tutorial, - action = "maj") + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + tuto=part.tutorial, + action="maj") elif "delete" in request.POST: # Delete all chapters belonging to the part @@ -1343,13 +1499,12 @@ def modify_part(request): new_slug_tuto_path = os.path.join(settings.REPO_PATH, part.tutorial.get_phy_slug()) # Actually delete the part part.delete() - - + maj_repo_tuto(request, - old_slug_path = new_slug_tuto_path, - new_slug_path = new_slug_tuto_path, - tuto = part.tutorial, - action = "maj") + old_slug_path=new_slug_tuto_path, + new_slug_path=new_slug_tuto_path, + tuto=part.tutorial, + action="maj") return redirect(part.tutorial.get_absolute_url()) @@ -1374,17 +1529,17 @@ def edit_part(request): if form.is_valid(): data = form.data # avoid collision - if content_has_changed([introduction, conclusion],data["last_hash"]): + if content_has_changed([introduction, conclusion], data["last_hash"]): form = PartForm({"title": part.title, - "introduction": part.get_introduction(), - "conclusion": part.get_conclusion()}) - return render_template("tutorial/part/edit.html", - { - "part": part, - "last_hash": compute_hash([introduction, conclusion]), - "new_version":True, - "form": form - }) + "introduction": part.get_introduction(), + "conclusion": part.get_conclusion()}) + return render_template("tutorial/part/edit.html", + { + "part": part, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True, + "form": form + }) # Update title and his slug. part.title = data["title"] @@ -1396,9 +1551,9 @@ def edit_part(request): part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") part.save() part.update_children() - + new_slug = os.path.join(settings.REPO_PATH, part.tutorial.get_phy_slug(), part.get_phy_slug()) - + maj_repo_part( request, old_slug_path=old_slug, @@ -1414,11 +1569,11 @@ def edit_part(request): "introduction": part.get_introduction(), "conclusion": part.get_conclusion()}) return render_template("tutorial/part/edit.html", - { - "part": part, - "last_hash": compute_hash([introduction, conclusion]), - "form": form - }) + { + "part": part, + "last_hash": compute_hash([introduction, conclusion]), + "form": form + }) # Chapters. @@ -1437,12 +1592,12 @@ def view_chapter( """View chapter.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - + try: sha = request.GET["version"] except KeyError: sha = tutorial.sha_draft - + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() # Only authors of the tutorial and staff can view tutorial in offline. @@ -1490,7 +1645,7 @@ def view_chapter( chapter["introduction"]) chapter["conclu"] = get_blob(repo.commit(sha).tree, chapter["conclusion"]) - + cpt_e = 1 for ext in chapter["extracts"]: ext["chapter"] = chapter @@ -1504,7 +1659,7 @@ def view_chapter( final_position = len(chapter_tab) - 1 cpt_c += 1 cpt_p += 1 - + # if chapter can't find if not find: raise Http404 @@ -1513,7 +1668,7 @@ def view_chapter( > 0 else None) next_chapter = (chapter_tab[final_position + 1] if final_position + 1 < len(chapter_tab) else None) - + return render_template("tutorial/chapter/view.html", { "tutorial": mandata, "chapter": final_chapter, @@ -1523,7 +1678,6 @@ def view_chapter( }) - def view_chapter_online( request, tutorial_pk, @@ -1551,7 +1705,7 @@ def view_chapter_online( final_chapter = None chapter_tab = [] final_position = 0 - + find = False for part in parts: cpt_c = 1 @@ -1612,14 +1766,14 @@ def view_chapter_online( final_position = len(chapter_tab) - 1 cpt_c += 1 cpt_p += 1 - + # if chapter can't find if not find: raise Http404 prev_chapter = (chapter_tab[final_position - 1] if final_position > 0 else None) next_chapter = (chapter_tab[final_position + 1] if final_position + 1 < len(chapter_tab) else None) - + return render_template("tutorial/chapter/view_online.html", { "chapter": final_chapter, "parts": parts, @@ -1664,7 +1818,7 @@ def add_chapter(request): img.pubdate = datetime.now() img.save() chapter.image = img - + chapter.save() if chapter.tutorial: chapter_path = os.path.join( @@ -1675,12 +1829,15 @@ def add_chapter(request): chapter.conclusion = os.path.join(chapter.get_phy_slug(), "conclusion.md") else: - chapter_path = os.path.join(settings.REPO_PATH, + chapter_path = os.path.join(settings.REPO_PATH, chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), + chapter.part.get_phy_slug(), chapter.get_phy_slug()) - chapter.introduction = os.path.join(chapter.part.get_phy_slug(),chapter.get_phy_slug(),"introduction.md") - chapter.conclusion = os.path.join(chapter.part.get_phy_slug(),chapter.get_phy_slug(),"conclusion.md") + chapter.introduction = os.path.join( + chapter.part.get_phy_slug(), + chapter.get_phy_slug(), + "introduction.md") + chapter.conclusion = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug(), "conclusion.md") chapter.save() maj_repo_chapter( request, @@ -1720,7 +1877,8 @@ def modify_chapter(request): # Make sure the user is allowed to do that - if request.user not in chapter.get_tutorial().authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + if request.user not in chapter.get_tutorial().authors.all() and \ + not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied if "move" in data: try: @@ -1733,25 +1891,25 @@ def modify_chapter(request): move(chapter, new_pos, "position_in_part", "part", "get_chapters") chapter.update_position_in_tutorial() chapter.save() - + new_slug_path = os.path.join(settings.REPO_PATH, chapter.part.tutorial.get_phy_slug()) - + maj_repo_part(request, - old_slug_path = new_slug_path, - new_slug_path = new_slug_path, - part = chapter.part, - action = "maj") - + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + part=chapter.part, + action="maj") + messages.info(request, u"Le chapitre a bien été déplacé.") elif "delete" in data: old_pos = chapter.position_in_part old_tut_pos = chapter.position_in_tutorial - + if chapter.part: parent = chapter.part else: parent = chapter.tutorial - + # Move other chapters first for tut_c in chapter.part.get_chapters(): @@ -1771,12 +1929,12 @@ def modify_chapter(request): Chapter.objects.filter(position_in_tutorial__gt=old_tut_pos): tut_c.update_position_in_tutorial() tut_c.save() - + maj_repo_part(request, - old_slug_path = new_slug_path_part, - new_slug_path = new_slug_path_part, - part = chapter.part, - action = "maj") + old_slug_path=new_slug_path_part, + new_slug_path=new_slug_path_part, + part=chapter.part, + action="maj") messages.info(request, u"Le chapitre a bien été supprimé.") return redirect(parent.get_absolute_url()) @@ -1799,14 +1957,14 @@ def edit_chapter(request): # Make sure the user is allowed to do that - if (big and request.user not in chapter.part.tutorial.authors.all() \ - or small and request.user not in chapter.tutorial.authors.all())\ - and not request.user.has_perm("tutorial.change_tutorial"): + if (big and request.user not in chapter.part.tutorial.authors.all() + or small and request.user not in chapter.tutorial.authors.all())\ + and not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied introduction = os.path.join(chapter.get_path(), "introduction.md") conclusion = os.path.join(chapter.get_path(), "conclusion.md") if request.method == "POST": - + if chapter.part: form = ChapterForm(request.POST, request.FILES) gal = chapter.part.tutorial.gallery @@ -1818,19 +1976,19 @@ def edit_chapter(request): # avoid collision if content_has_changed([introduction, conclusion], data["last_hash"]): form = render_chapter_form(chapter) - return render_template("tutorial/part/edit.html", - { - "chapter": chapter, - "last_hash": compute_hash([introduction, conclusion]), - "new_version":True, - "form": form - }) + return render_template("tutorial/part/edit.html", + { + "chapter": chapter, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True, + "form": form + }) chapter.title = data["title"] - + old_slug = chapter.get_path() chapter.save() chapter.update_children() - + if chapter.part: if chapter.tutorial: new_slug = os.path.join(settings.REPO_PATH, chapter.tutorial.get_phy_slug(), chapter.get_phy_slug()) @@ -1868,7 +2026,6 @@ def edit_chapter(request): "form": form}) - @login_required def add_extract(request): """Add extract.""" @@ -1956,9 +2113,9 @@ def edit_extract(request): if "preview" in data: form = ExtractForm(initial={ - "title": data["title"], - "text": data["text"] - }) + "title": data["title"], + "text": data["text"] + }) return render_template("tutorial/extract/edit.html", { "extract": extract, "form": form, @@ -1969,13 +2126,13 @@ def edit_extract(request): form = ExtractForm(initial={ "title": extract.title, "text": extract.get_text()}) - return render_template("tutorial/extract/edit.html", - { - "extract": extract, - "last_hash": compute_hash([extract.get_path()]), - "new_version":True, - "form": form - }) + return render_template("tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "new_version": True, + "form": form + }) # Edit extract. form = ExtractForm(request.POST) @@ -1985,24 +2142,10 @@ def edit_extract(request): extract.title = data["title"] extract.text = extract.get_path(relative=True) - # Get path for mini-tuto - - if extract.chapter.tutorial: - chapter_tutorial_path = os.path.join(settings.REPO_PATH, extract.chapter.tutorial.get_phy_slug()) - chapter_part = os.path.join(chapter_tutorial_path) - else: - - # Get path for big-tuto - - chapter_part_tutorial_path = \ - os.path.join(settings.REPO_PATH, extract.chapter.part.tutorial.get_phy_slug()) - chapter_part_path = os.path.join(chapter_part_tutorial_path, extract.chapter.part.get_phy_slug()) - chapter_part = os.path.join(chapter_part_path, extract.chapter.get_phy_slug()) - # Use path retrieve before and use it to create the new slug. extract.save() new_slug = extract.get_path() - + maj_repo_extract( request, old_slug_path=old_slug, @@ -2015,12 +2158,12 @@ def edit_extract(request): else: form = ExtractForm({"title": extract.title, "text": extract.get_text()}) - return render_template("tutorial/extract/edit.html", - { - "extract": extract, - "last_hash": compute_hash([extract.get_path()]), - "form": form - }) + return render_template("tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "form": form + }) @can_write_and_read_now @@ -2042,41 +2185,27 @@ def modify_extract(request): - 1 extract_c.save() - # Get path for mini-tuto - - if extract.chapter.tutorial: - chapter_tutorial_path = os.path.join(settings.REPO_PATH, extract.chapter.tutorial.get_phy_slug()) - chapter_path = os.path.join(chapter_tutorial_path) - else: - - # Get path for big-tuto - - chapter_part_tutorial_path = os.path.join( - settings.REPO_PATH, extract.chapter.part.tutorial.get_phy_slug()) - chapter_part_path = os.path.join(chapter_part_tutorial_path, extract.chapter.part.get_phy_slug()) - chapter_path = os.path.join(chapter_part_path, extract.chapter.get_phy_slug()) - # Use path retrieve before and use it to create the new slug. old_slug = extract.get_path() - + if extract.chapter.tutorial: new_slug_path_chapter = os.path.join(settings.REPO_PATH, - extract.chapter.tutorial.get_phy_slug()) + extract.chapter.tutorial.get_phy_slug()) else: new_slug_path_chapter = os.path.join(settings.REPO_PATH, - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) maj_repo_extract(request, old_slug_path=old_slug, extract=extract, action="del") - + maj_repo_chapter(request, - old_slug_path = new_slug_path_chapter, - new_slug_path = new_slug_path_chapter, - chapter = chapter, - action = "maj") + old_slug_path=new_slug_path_chapter, + new_slug_path=new_slug_path_chapter, + chapter=chapter, + action="maj") return redirect(chapter.get_absolute_url()) elif "move" in data: try: @@ -2084,10 +2213,10 @@ def modify_extract(request): except ValueError: # Error, the user misplayed with the move button return redirect(extract.get_absolute_url()) - + move(extract, new_pos, "position_in_chapter", "chapter", "get_extracts") extract.save() - + if extract.chapter.tutorial: new_slug_path = os.path.join(settings.REPO_PATH, extract.chapter.tutorial.get_phy_slug()) @@ -2098,15 +2227,14 @@ def modify_extract(request): chapter.get_phy_slug()) maj_repo_chapter(request, - old_slug_path = new_slug_path, - new_slug_path = new_slug_path, - chapter = chapter, - action = "maj") + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + chapter=chapter, + action="maj") return redirect(extract.get_absolute_url()) raise Http404 - def find_tuto(request, pk_user): try: type = request.GET["type"] @@ -2117,7 +2245,7 @@ def find_tuto(request, pk_user): tutorials = Tutorial.objects.all().filter( authors__in=[display_user], sha_beta__isnull=False).exclude(sha_beta="").order_by("-pubdate") - + tuto_versions = [] for tutorial in tutorials: mandata = tutorial.load_json_for_public(sha=tutorial.sha_beta) @@ -2130,7 +2258,7 @@ def find_tuto(request, pk_user): tutorials = Tutorial.objects.all().filter( authors__in=[display_user], sha_public__isnull=False).exclude(sha_public="").order_by("-pubdate") - + tuto_versions = [] for tutorial in tutorials: mandata = tutorial.load_json_for_public() @@ -2251,7 +2379,7 @@ def import_content( part.save() part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") - part_path = os.path.join(settings.REPO_PATH, part.tutorial.get_phy_slug(),part.get_phy_slug()) + part_path = os.path.join(settings.REPO_PATH, part.tutorial.get_phy_slug(), part.get_phy_slug()) part.save() maj_repo_part( request, @@ -2428,31 +2556,49 @@ def local_import(request): @login_required def import_tuto(request): if request.method == "POST": - form = ImportForm(request.POST, request.FILES) - - # check extension - - if "file" in request.FILES: - filename = str(request.FILES["file"]) - ext = filename.split(".")[-1] - if ext == "tuto": - import_content(request, request.FILES["file"], - request.FILES["images"], "") + # for import tuto + if "import-tuto" in request.POST: + form = ImportForm(request.POST, request.FILES) + form_archive = ImportArchiveForm(user=request.user) + if "file" in request.FILES: + filename = str(request.FILES["file"]) + ext = filename.split(".")[-1] + if ext == "tuto": + import_content(request, request.FILES["file"], + request.FILES["images"], "") + else: + raise Http404 + return redirect(reverse("zds.member.views.tutorials")) + elif "import-archive" in request.POST: + form = ImportForm() + form_archive = ImportArchiveForm(request.user, request.POST, request.FILES) + if form_archive.is_valid(): + (check, reason) = import_archive(request) + if not check: + messages.error(request, reason) + else: + messages.success(request, reason) + return redirect(reverse("zds.member.views.tutorials")) else: - raise Http404 - return redirect(reverse("zds.member.views.tutorials")) + return render_template("tutorial/tutorial/import.html", + {"form": form, + "form_archive": form_archive}) else: form = ImportForm() - profile = get_object_or_404(Profile, user=request.user) - oldtutos = [] - if profile.sdz_tutorial: - olds = profile.sdz_tutorial.strip().split(":") - else: - olds = [] - for old in olds: - oldtutos.append(get_info_old_tuto(old)) + form_archive = ImportArchiveForm(user=request.user) + + profile = get_object_or_404(Profile, user=request.user) + oldtutos = [] + if profile.sdz_tutorial: + olds = profile.sdz_tutorial.strip().split(":") + else: + olds = [] + for old in olds: + oldtutos.append(get_info_old_tuto(old)) return render_template( - "tutorial/tutorial/import.html", {"form": form, "old_tutos": oldtutos}) + "tutorial/tutorial/import.html", {"form": form, + "form_archive": form_archive, + "old_tutos": oldtutos}) # Handling repo @@ -2550,7 +2696,7 @@ def maj_repo_part( conclu.write(smart_str(conclusion).strip()) conclu.close() index.add([os.path.join(part.get_path(relative=True), "conclusion.md" - )]) + )]) aut_user = str(request.user.pk) aut_email = str(request.user.email) if aut_email is None or aut_email.strip() == "": @@ -2599,14 +2745,14 @@ def maj_repo_chapter( os.makedirs(new_slug_path, mode=0o777) msg = "Creation du chapitre" if introduction is not None: - intro = open(os.path.join(new_slug_path, "introduction.md"), "w") - intro.write(smart_str(introduction).strip()) - intro.close() + intro = open(os.path.join(new_slug_path, "introduction.md"), "w") + intro.write(smart_str(introduction).strip()) + intro.close() if conclusion is not None: conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") conclu.write(smart_str(conclusion).strip()) conclu.close() - if ph != None: + if ph is not None: index.add([ph]) # update manifest @@ -2654,9 +2800,9 @@ def maj_repo_extract( else: repo = Repo(os.path.join(settings.REPO_PATH, extract.chapter.part.tutorial.get_phy_slug())) index = repo.index - + chap = extract.chapter - + if action == "del": msg = "Suppression de l'exrait " extract.delete() @@ -2704,20 +2850,35 @@ def maj_repo_extract( chap.part.tutorial.save() +def insert_into_zip(zip_file, git_tree): + """recursively add files from a git_tree to a zip archive""" + for blob in git_tree.blobs: # first, add files : + zip_file.writestr(blob.path, blob.data_stream.read()) + if len(git_tree.trees) is not 0: # then, recursively add dirs : + for subtree in git_tree.trees: + insert_into_zip(zip_file, subtree) + def download(request): """Download a tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) - ph = os.path.join(settings.REPO_PATH, tutorial.get_phy_slug()) - repo = Repo(ph) - repo.archive(open(ph + ".tar", "w")) - response = HttpResponse(open(ph + ".tar", "rb").read(), - mimetype="application/tar") - response["Content-Disposition"] = \ - "attachment; filename={0}.tar".format(tutorial.slug) - return response + repo_path = os.path.join(settings.REPO_PATH, tutorial.get_phy_slug()) + repo = Repo(repo_path) + sha = tutorial.sha_draft + if 'online' in request.GET and tutorial.on_line(): + sha = tutorial.sha_public + elif request.user not in tutorial.authors.all(): + if not request.user.has_perm('tutorial.change_tutorial'): + raise PermissionDenied # Only authors can download draft version + zip_path = os.path.join(tempfile.gettempdir(), tutorial.slug + '.zip') + zip_file = zipfile.ZipFile(zip_path, 'w') + insert_into_zip(zip_file, repo.commit(sha).tree) + zip_file.close() + response = HttpResponse(open(zip_path, "rb").read(), content_type="application/zip") + response["Content-Disposition"] = "attachment; filename={0}.zip".format(tutorial.slug) + os.remove(zip_path) + return response @permission_required("tutorial.change_tutorial", raise_exception=True) @@ -2726,69 +2887,66 @@ def download_markdown(request): tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) phy_path = os.path.join( - tutorial.get_prod_path(), - tutorial.slug + - ".md") + tutorial.get_prod_path(), + tutorial.slug + + ".md") response = HttpResponse( open(phy_path, "rb").read(), - mimetype="application/txt") + content_type="application/txt") response["Content-Disposition"] = \ "attachment; filename={0}.md".format(tutorial.slug) return response - def download_html(request): """Download a pdf tutorial.""" tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) phy_path = os.path.join( - tutorial.get_prod_path(), - tutorial.slug + - ".html") + tutorial.get_prod_path(), + tutorial.slug + + ".html") if not os.path.isfile(phy_path): raise Http404 response = HttpResponse( open(phy_path, "rb").read(), - mimetype="text/html") + content_type="text/html") response["Content-Disposition"] = \ "attachment; filename={0}.html".format(tutorial.slug) return response - def download_pdf(request): """Download a pdf tutorial.""" tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) phy_path = os.path.join( - tutorial.get_prod_path(), - tutorial.slug + - ".pdf") + tutorial.get_prod_path(), + tutorial.slug + + ".pdf") if not os.path.isfile(phy_path): raise Http404 response = HttpResponse( open(phy_path, "rb").read(), - mimetype="application/pdf") + content_type="application/pdf") response["Content-Disposition"] = \ "attachment; filename={0}.pdf".format(tutorial.slug) return response - def download_epub(request): """Download an epub tutorial.""" tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) phy_path = os.path.join( - tutorial.get_prod_path(), - tutorial.slug + - ".epub") + tutorial.get_prod_path(), + tutorial.slug + + ".epub") if not os.path.isfile(phy_path): raise Http404 response = HttpResponse( open(phy_path, "rb").read(), - mimetype="application/epub") + content_type="application/epub") response["Content-Disposition"] = \ "attachment; filename={0}.epub".format(tutorial.slug) return response @@ -2805,26 +2963,26 @@ def get_url_images(md_text, pt): if md_text is not None: imgs = re.findall(regex, md_text) for img in imgs: - real_url=img[1] - # decompose images + real_url = img[1] + # decompose images parse_object = urlparse(real_url) - if parse_object.query!='': + if parse_object.query != '': resp = parse_qs(urlparse(img[1]).query, keep_blank_values=True) real_url = resp["u"][0] parse_object = urlparse(real_url) # if link is http type if parse_object.scheme in ["http", "https", "ftp"] or \ - parse_object.netloc[:3]=="www" or \ - parse_object.path[:3]=="www": + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": (filepath, filename) = os.path.split(parse_object.path) if not os.path.isdir(os.path.join(pt, "images")): os.makedirs(os.path.join(pt, "images")) # download image - down_path=os.path.abspath(os.path.join(pt, "images", filename)) + down_path = os.path.abspath(os.path.join(pt, "images", filename)) try: - urlretrieve(real_url, down_path) + urlretrieve(real_url, down_path) try: ext = filename.split(".")[-1] im = ImagePIL.open(down_path) @@ -2849,6 +3007,7 @@ def get_url_images(md_text, pt): if not os.path.exists(dstdir): os.makedirs(dstdir) shutil.copy(srcfile, dstroot) + try: ext = dstroot.split(".")[-1] im = ImagePIL.open(dstroot) @@ -2868,39 +3027,39 @@ def sub_urlimg(g): start = g.group("start") url = g.group("url") parse_object = urlparse(url) - if parse_object.query!='': + if parse_object.query != '': resp = parse_qs(urlparse(url).query, keep_blank_values=True) parse_object = urlparse(resp["u"][0]) (filepath, filename) = os.path.split(parse_object.path) - if filename!='': - mark= g.group("mark") + if filename != '': + mark = g.group("mark") ext = filename.split(".")[-1] if ext == "gif": if parse_object.scheme in ("http", "https") or \ - parse_object.netloc[:3]=="www" or \ - parse_object.path[:3]=="www": + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": url = os.path.join("images", filename.split(".")[0] + ".png") else: url = (url.split(".")[0])[1:] + ".png" else: if parse_object.scheme in ("http", "https") or \ - parse_object.netloc[:3]=="www" or \ - parse_object.path[:3]=="www": + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": url = os.path.join("images", filename) else: url = url[1:] end = g.group("end") - return start + mark+ url + end + return start + mark + url + end else: return start - + def markdown_to_out(md_text): return re.sub(ur"(?P)(?P!\[.*?\]\()(?P.+?)(?P\))", sub_urlimg, md_text) -def MEP(tutorial, sha): +def mep(tutorial, sha): (output, err) = (None, None) repo = Repo(tutorial.get_path()) manifest = get_blob(repo.commit(sha).tree, "manifest.json") @@ -2911,8 +3070,8 @@ def MEP(tutorial, sha): except: shutil.rmtree(u"\\\\?\{0}".format(tutorial.get_prod_path())) shutil.copytree(tutorial.get_path(), tutorial.get_prod_path()) - repo.head.reset(commit = sha, index=True, working_tree=True) - + repo.head.reset(commit=sha, index=True, working_tree=True) + # collect md files fichiers = [] @@ -2971,11 +3130,11 @@ def MEP(tutorial, sha): out_file.close() # define whether to log pandoc's errors - + pandoc_debug_str = "" if settings.PANDOC_LOG_STATE: - pandoc_debug_str = " 2>&1 | tee -a "+settings.PANDOC_LOG - + pandoc_debug_str = " 2>&1 | tee -a " + settings.PANDOC_LOG + # load pandoc os.chdir(tutorial.get_prod_path()) @@ -2983,25 +3142,25 @@ def MEP(tutorial, sha): + "pandoc --latex-engine=xelatex -s -S --toc " + os.path.join(tutorial.get_prod_path(), tutorial.slug) + ".md -o " + os.path.join(tutorial.get_prod_path(), - tutorial.slug) + ".html"+pandoc_debug_str) + tutorial.slug) + ".html" + pandoc_debug_str) os.system(settings.PANDOC_LOC + "pandoc " + "--latex-engine=xelatex " + "--template=../../assets/tex/template.tex " + "-s " + "-S " + "-N " + "--toc " + "-V documentclass=scrbook " - + "-V lang=francais " + "-V mainfont=Verdana " + + "-V lang=francais " + "-V mainfont=Merriweather " + "-V monofont=\"Andale Mono\" " + "-V fontsize=12pt " + "-V geometry:margin=1in " + os.path.join(tutorial.get_prod_path(), tutorial.slug) + ".md " + "-o " + os.path.join(tutorial.get_prod_path(), tutorial.slug) - + ".pdf"+pandoc_debug_str) + + ".pdf" + pandoc_debug_str) os.system(settings.PANDOC_LOC + "pandoc -s -S --toc " + os.path.join(tutorial.get_prod_path(), tutorial.slug) + ".md -o " + os.path.join(tutorial.get_prod_path(), - tutorial.slug) + ".epub"+pandoc_debug_str) + tutorial.slug) + ".epub" + pandoc_debug_str) os.chdir(settings.SITE_ROOT) return (output, err) -def UNMEP(tutorial): +def un_mep(tutorial): if os.path.isdir(tutorial.get_prod_path()): try: shutil.rmtree(tutorial.get_prod_path()) @@ -3044,6 +3203,11 @@ def answer(request): if tutorial.last_note: last_note_pk = tutorial.last_note.pk + # Retrieve lasts notes of the current tutorial. + notes = Note.objects.filter(tutorial=tutorial) \ + .prefetch_related() \ + .order_by("-pubdate")[:settings.POSTS_PER_PAGE] + # User would like preview his post or post a new note on the tutorial. if request.method == "POST": @@ -3059,6 +3223,7 @@ def answer(request): "tutorial": tutorial, "last_note_pk": last_note_pk, "newnote": newnote, + "notes": notes, "form": form, }) else: @@ -3085,12 +3250,12 @@ def answer(request): "tutorial": tutorial, "last_note_pk": last_note_pk, "newnote": newnote, + "notes": notes, "form": form, }) else: # Actions from the editor render to answer.html. - text = "" # Using the quote button @@ -3130,16 +3295,16 @@ def solve_alert(request): bot = get_object_or_404(User, username=settings.BOT_ACCOUNT) msg = \ (u'Bonjour {0},' - u'Vous recevez ce message car vous avez signalé le message de *{1}*, ' - u'dans le tutoriel [{2}]({3}). Votre alerte a été traitée par **{4}** ' - u'et il vous a laissé le message suivant :' - u'\n\n> {5}\n\nToute l\'équipe de la modération vous remercie !'.format( - alert.author.username, - note.author.username, - note.tutorial.title, - settings.SITE_URL + note.get_absolute_url(), - request.user.username, - request.POST["text"],)) + u'Vous recevez ce message car vous avez signalé le message de *{1}*, ' + u'dans le tutoriel [{2}]({3}). Votre alerte a été traitée par **{4}** ' + u'et il vous a laissé le message suivant :' + u'\n\n> {5}\n\nToute l\'équipe de la modération vous remercie !'.format( + alert.author.username, + note.author.username, + note.tutorial.title, + settings.SITE_URL + note.get_absolute_url(), + request.user.username, + request.POST["text"],)) send_mp( bot, [alert.author], diff --git a/zds/utils/__init__.py b/zds/utils/__init__.py index 11a1c603d1..e0643a1b92 100644 --- a/zds/utils/__init__.py +++ b/zds/utils/__init__.py @@ -3,7 +3,7 @@ from django.template import RequestContext, defaultfilters from django.shortcuts import render_to_response -from git import * +from git import Repo from django.conf import settings @@ -22,15 +22,17 @@ def get_current_user(): def get_current_request(): return getattr(_thread_locals, 'request', None) + def get_git_version(): - try : + try: repo = Repo(settings.SITE_ROOT) branch = repo.active_branch commit = repo.head.commit.hexsha v = u"{0}/{1}".format(branch, commit[:7]) - return {'name':v, 'url':u'https://github.com/zestedesavoir/zds-site/tree/{0}'.format(commit)} + return {'name': v, 'url': u'https://github.com/zestedesavoir/zds-site/tree/{0}'.format(commit)} except: - return {'name':'', 'url':''} + return {'name': '', 'url': ''} + class ThreadLocals(object): @@ -42,7 +44,7 @@ def process_request(self, request): def render_template(tmpl, dct=None): if dct is None: dct = {} - dct['git_version']=get_git_version() + dct['git_version'] = get_git_version() return render_to_response( tmpl, dct, context_instance=RequestContext(get_current_request())) diff --git a/zds/utils/articles.py b/zds/utils/articles.py index ec1e176c0b..1f373c8081 100644 --- a/zds/utils/articles.py +++ b/zds/utils/articles.py @@ -2,8 +2,6 @@ from collections import OrderedDict -from git import * - import os diff --git a/zds/utils/forums.py b/zds/utils/forums.py new file mode 100644 index 0000000000..1abd0151cd --- /dev/null +++ b/zds/utils/forums.py @@ -0,0 +1,78 @@ +# coding: utf-8 + +from datetime import datetime +from zds.forum.models import Topic, Post, follow +from zds.forum.views import get_tag_by_title +from zds.utils.templatetags.emarkdown import emarkdown + + +def create_topic( + author, + forum, + title, + subtitle, + text, + key): + """create topic in forum""" + + (tags, title_only) = get_tag_by_title(title[:80]) + + # Creating the thread + n_topic = Topic() + n_topic.forum = forum + n_topic.title = title_only + n_topic.subtitle = subtitle + n_topic.pubdate = datetime.now() + n_topic.author = author + n_topic.key = key + n_topic.save() + n_topic.add_tags(tags) + n_topic.save() + + # Add the first message + post = Post() + post.topic = n_topic + post.author = author + post.text = text + post.text_html = emarkdown(text) + post.pubdate = datetime.now() + post.position = 1 + post.save() + + n_topic.last_message = post + n_topic.save() + + follow(n_topic, user=author) + + return n_topic + + +def send_post(topic, text): + + post = Post() + post.topic = topic + post.author = topic.author + post.text = text + post.text_html = emarkdown(text) + post.pubdate = datetime.now() + post.position = topic.last_message.position + 1 + post.save() + + topic.last_message = post + topic.save() + + +def lock_topic(topic): + topic.is_locked = True + topic.save() + + +def unlock_topic(topic, msg): + topic.is_locked = False + main = Post.objects.filter(topic__pk=topic.pk, position=1).first() + main.text = msg + main.text_html = emarkdown(msg) + main.editor = topic.author + main.update = datetime.now() + main.save() + topic.save() diff --git a/zds/utils/misc.py b/zds/utils/misc.py index f3ddea168e..d99025106f 100644 --- a/zds/utils/misc.py +++ b/zds/utils/misc.py @@ -1,8 +1,4 @@ # coding: utf-8 - -import os -import string -import uuid import hashlib THUMB_MAX_WIDTH = 80 @@ -11,6 +7,7 @@ MEDIUM_MAX_WIDTH = 200 MEDIUM_MAX_HEIGHT = 200 + def compute_hash(filenames): """returns a md5 hexdigest of group of files to check if they have change""" md5_hash = hashlib.md5() @@ -25,29 +22,10 @@ def compute_hash(filenames): md5_hash.update(read_bytes) return md5_hash.hexdigest() + def content_has_changed(filenames, md5): return md5 != compute_hash(filenames) -def image_path(instance, filename): - """Return path to an image.""" - ext = filename.split('.')[-1] - filename = u'{}.{}'.format(str(uuid.uuid4()), string.lower(ext)) - return os.path.join('articles', str(instance.pk), filename) - - -def thumb_path(instance, filename): - """Return path to an image.""" - ext = filename.split('.')[-1] - filename = u'{}.{}'.format(str(uuid.uuid4()), string.lower(ext)) - return os.path.join('articles', str(instance.pk), filename) - - -def medium_path(instance, filename): - """Return path to an image.""" - ext = filename.split('.')[-1] - filename = u'{}.{}'.format(str(uuid.uuid4()), string.lower(ext)) - return os.path.join('articles', str(instance.pk), filename) - def has_changed(instance, field, manager='objects'): """Returns true if a field has changed in a model May be used in a diff --git a/zds/utils/models.py b/zds/utils/models.py index d713e82ee9..1c6c4e218e 100644 --- a/zds/utils/models.py +++ b/zds/utils/models.py @@ -9,6 +9,7 @@ from django.utils.encoding import smart_text from django.db import models from zds.utils import slugify +from zds.utils.templatetags.emarkdown import emarkdown from model_utils.managers import InheritanceManager @@ -67,7 +68,7 @@ def get_subcategories(self): .filter(category__in=[self], is_main=True)\ .select_related('subcategory')\ .all() - + for catsubcat in catsubcats: if catsubcat.subcategory.get_tutos().count() > 0: csc.append(catsubcat) @@ -195,6 +196,10 @@ def get_dislike_count(self): """Gets number of dislike for the post.""" return CommentDislike.objects.filter(comments__pk=self.pk).count() + def update_content(self, text): + self.text = text + self.text_html = emarkdown(self.text) + class Alert(models.Model): @@ -276,6 +281,11 @@ def __unicode__(self): """Textual Link Form.""" return u"{0}".format(self.title) + def get_absolute_url(self): + return reverse('zds.forum.views.find_topic_by_tag', + kwargs={'tag_pk': self.pk, + 'tag_slug': self.slug}) + def save(self, *args, **kwargs): self.title = smart_text(self.title).lower() self.slug = slugify(self.title) diff --git a/zds/utils/mps.py b/zds/utils/mps.py index 5e6ce4272e..06ce234527 100644 --- a/zds/utils/mps.py +++ b/zds/utils/mps.py @@ -18,7 +18,13 @@ def send_mp( send_by_mail=True, leave=True, direct=False): - """Send MP at members.""" + """ + Send MP at members. + + Most of the param are obvious, excepted : + * direct : send a mail directly without mp (ex : ban members who wont connect again) + * leave : the author leave the conversation (usefull for the bot : it wont read the response a member could send) + """ # Creating the thread n_topic = PrivateTopic() diff --git a/zds/utils/templatetags/append_to_get.py b/zds/utils/templatetags/append_to_get.py index 217ff11353..a6cfacf37e 100644 --- a/zds/utils/templatetags/append_to_get.py +++ b/zds/utils/templatetags/append_to_get.py @@ -1,46 +1,102 @@ +# -*- coding: utf-8 -*- + from django import template +from functools import wraps register = template.Library() """ -Decorator to facilitate template tag creation +Decorator to facilitate template tag creation. """ + + def easy_tag(func): - """deal with the repetitive parts of parsing template tags""" - def inner(parser, token): + """ + Deal with the repetitive parts of parsing template tags : + + - Wraps functions attributes; + - Raise `TemplateSyntaxError` if arguments are not well formatted. + + :rtype: function + :param func: Function to wraps. + :type func: function + """ + + @wraps(func) + def inner(_, token): + split_arg = token.split_contents() try: - return func(*token.split_contents()) + return func(*split_arg) except TypeError: - raise template.TemplateSyntaxError('Bad arguments for tag "%s"' % token.split_contents()[0]) - inner.__name__ = func.__name__ - inner.__doc__ = inner.__doc__ - return inner + import inspect + args = inspect.getargspec(func).args[1:] + err_msg = 'Bad arguments for tag "{0}".\nThe tag "{0}" take {1} arguments ({2}).\n {3} were provided ({4}).' + fstring = err_msg.format(split_arg[0], + len(args), + ", ".join(args), + len(split_arg), + ", ".join(split_arg)) + raise template.TemplateSyntaxError(fstring) + return inner class AppendGetNode(template.Node): - def __init__(self, dict): - self.dict_pairs = {} - for pair in dict.split(','): - pair = pair.split('=') - self.dict_pairs[pair[0]] = template.Variable(pair[1]) - + """ + Template node allowing to render an URL appending argument to current GET address. + + Parse a string like `key1=var1,key2=var2` and generate a new URL with the provided parameters appended to current + parameters. + """ + + def __init__(self, arg_list): + """ + Create a template node which append `arg_list` to GET URL. + + :param str arg_list: the argument list to append. + """ + + self.__dict_pairs = {} + for pair in arg_list.split(','): + if pair: + try: + key, val = pair.split('=') + if not val: + raise template.TemplateSyntaxError( + "Bad argument format. Empty value for key '{}".format(key)) + self.__dict_pairs[key] = template.Variable(val) + except ValueError: + raise template.TemplateSyntaxError( + "Bad argument format.\n'{}' must use the format 'key1=var1,key2=var2'".format(arg_list)) + def render(self, context): + """ + Render the new URL according to the current context. + + :param context: Current context. + :return: New URL with arguments appended. + :rtype: str + """ get = context['request'].GET.copy() path = context['request'].META['PATH_INFO'] - for key in self.dict_pairs: - get[key] = self.dict_pairs[key].resolve(context) - - if len(get): - path += "?" - for (key, v) in get.items(): - for value in get.getlist(key): - path += u"&{0}={1}".format(key, value) - + for key in self.__dict_pairs: + get[key] = self.__dict_pairs[key].resolve(context) + + if len(get) > 0: + list_arg = [u"{0}={1}".format(key, value) for key in get.keys() for value in get.getlist(key)] + path += u"?" + u"&".join(list_arg) + return path + @register.tag() @easy_tag -def append_to_get(_tag_name, dict): - return AppendGetNode(dict) +def append_to_get(_, arg_list): + """Render an URL appending argument to current GET address. + + :param _: Tag name (not used) + :param arg_list: Argument list like `key1=var1,key2=var2` + :return: Template node. + """ + return AppendGetNode(arg_list) diff --git a/zds/utils/templatetags/captureas.py b/zds/utils/templatetags/captureas.py index 1852cf6b9c..d9d0aa14f3 100644 --- a/zds/utils/templatetags/captureas.py +++ b/zds/utils/templatetags/captureas.py @@ -1,25 +1,58 @@ +# -*- coding: utf-8 -*- + from django import template register = template.Library() +""" +Define a tag allowing to capture template content as a variable. +""" + @register.tag(name='captureas') def do_captureas(parser, token): + """ + Define a tag allowing to capture template content as a variable. + + :param parser: The django template parser + :param token: tag token (tag_name + variable_name) + :return: Template node. + """ + try: - tag_name, args = token.contents.split(None, 1) + _, variable_name = token.split_contents() except ValueError: raise template.TemplateSyntaxError("'captureas' node requires a variable name.") + nodelist = parser.parse(('endcaptureas',)) parser.delete_first_token() - return CaptureasNode(nodelist, args) + + return CaptureasNode(nodelist, variable_name) class CaptureasNode(template.Node): - def __init__(self, nodelist, varname): - self.nodelist = nodelist - self.varname = varname + """ + Capture end render node content to a variable name. + """ + + def __init__(self, nodelist, variable_name): + """ + Create a template node which render `nodelist` to `variable_name`. + + :param nodelist: The node list to capture. + :param variable_name: The variable name which will gain the rendered content. + """ + self.__node_list = nodelist + self.__variable_name = variable_name def render(self, context): - output = self.nodelist.render(context) - context[self.varname] = output.strip(' \t\n\r') - return '' \ No newline at end of file + """ + Render the node list to the variable name. + + :param context: Current context. + :return: Empty string + :rtype: str + """ + output = self.__node_list.render(context) + context[self.__variable_name] = output.strip() + return '' diff --git a/zds/utils/templatetags/cssstyles.py b/zds/utils/templatetags/cssstyles.py deleted file mode 100644 index d090454ff3..0000000000 --- a/zds/utils/templatetags/cssstyles.py +++ /dev/null @@ -1,152 +0,0 @@ -# coding: utf-8 - -import random -import re - -from markdown.extensions import Extension -from markdown.postprocessors import Postprocessor - - -HTMLELEMENTS = ['p', 'a', 'div', 'blockquote', 'hr'] -HTMLATTRIBUTES = ['style', 'id', 'href', 'class'] -STYLE_REG = reg = re.compile(r'\[([a-z]+)\]\{(.+?)\r?\n\}', re.DOTALL) - -getStyle = lambda tag, blockcontent, blockid: { - 'secret': {'1hr': {}, - '7div': - {'class': - ['hidden-block'], - 'id': - ['show-' + str(blockid)], - 'text': - {'3p_': - {'text': - blockcontent - }, - '1a': - {'href': - '#show-' + str(blockid), - 'id': - 'display-' + str(blockid), - 'class': - ['hidden-link-show'], - 'text': - u'Contenu masqué (cliquez pour afficher)', - }, - '2a': - {'href': - '#hide-' + str(blockid), - 'id': - 'hide-' + str(blockid), - 'class': - ['hidden-link-hide'], - 'text': - 'Masquer le contenu', - }, - }, - }, - '4hr': {} - - - }, -}[tag] - - -class Element(): - __tag = None - __attrs = {} - __text = "" - - def __init__(self, tag): - self.__tag = tag - self.__attrs = {} - self.__text = "" - - def setattr(self, name, value): - self.__attrs[name] = unicode(value) - - def settext(self, text): - self.__text = unicode(text) - - def completeWith(self, elem): - for attr in elem.__attrs: - self.setattr(attr, elem.__attrs[attr]) - self.__text = elem.__text - - def __repr__(self): - def formatAttr(name, value): - return unicode(name) + '="' + unicode(value) + '"' - if len(self.__text) > 0: - return '<' + unicode(self.__tag) + ' ' + \ - ' '.join([formatAttr(attr, self.__attrs[attr]) for attr in self.__attrs.keys()] - ) + '>' + unicode(self.__text) + '' - else: - return '<' + unicode(self.__tag) + ' ' + ' '.join( - [formatAttr(attr, self.__attrs[attr]) for attr in self.__attrs.keys()]) + ' />' - - -def cleanElement(element_name): - return re.sub(r'(?:[0-9]*)([a-z]+)_*', r'\1', element_name) - - -def localProcess(elements, parent=None): - for elem in elements.keys(): - value = elements[elem] - if cleanElement(elem) in HTMLELEMENTS: - el = Element(cleanElement(elem)) - for e in localProcess(value, el): - el = e - yield el - elif elem in HTMLATTRIBUTES: - if isinstance(value, str) or isinstance(value, unicode): - parent.setattr(elem, value) - elif isinstance(value, list): - parent.setattr(elem, ' '.join(value)) - elif isinstance(value, dict): - parent.setattr(elem, ';'.join( - [':'.join([n, value[n]]) for n in value])) - else: - parent.setattr(elem, value) - yield parent - elif elem == 'text': - if isinstance(value, dict): - parent.settext( - ''.join([unicode(a) for a in localProcess(value)])) - elif isinstance(value, str) or isinstance(value, unicode): - parent.settext(unicode(value)) - elif isinstance(value, list): - parent.settext(' '.join(value)) - else: - parent.settext("") - yield parent - else: - yield parent - - -def processStyle(styles): - return ''.join([unicode(a) for a in localProcess(styles)]) - - -def process(match): - try: - return processStyle( - getStyle( - match.group(1), - match.group(2), - random.randint( - 1000, - 9999))) - except KeyError: - return "Style non-valide" - - -class CSSPostprocessor(Postprocessor): - - def run(self, text): - return re.sub(STYLE_REG, process, text, re.DOTALL) - - -class StyleExtension(Extension): - - def extendMarkdown(self, md, md_globals): - md.postprocessors.add('cssstyle', CSSPostprocessor(md), '_begin') diff --git a/zds/utils/templatetags/date.py b/zds/utils/templatetags/date.py index 15499f8c3a..751fc5bd16 100644 --- a/zds/utils/templatetags/date.py +++ b/zds/utils/templatetags/date.py @@ -1,6 +1,7 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- from datetime import timedelta +import time from django import template from django.contrib.humanize.templatetags.humanize import naturaltime @@ -8,43 +9,68 @@ from django.utils.datetime_safe import datetime from django.utils.tzinfo import LocalTimezone - register = template.Library() +""" +Define a filter to format date. +""" + +# Date formatting constants + +__DATE_FMT_FUTUR = "Dans le futur" +__ABS_DATE_FMT_SMALL = 'd/m/y à H\hi' # Small format +__ABS_DATE_FMT_NORMAL = 'l d F Y à H\hi' # Normal format +__ABS_HUMAN_TIME_FMT = "%d %b %Y, %H:%M:%S" + def date_formatter(value, tooltip, small): + """ + Format a date to an human readable string. + + :param value: Date to format. + :param bool tooltip: if `True`, format date to a tooltip label. + :param bool small: if `True`, create a shorter string. + :return: + """ try: value = datetime(value.year, value.month, value.day, value.hour, value.minute, value.second) - except AttributeError: - return value - except ValueError: + except (AttributeError, ValueError): + # todo : Check why not raise template.TemplateSyntaxError() ? return value if getattr(value, 'tzinfo', None): now = datetime.now(LocalTimezone(value)) else: now = datetime.now() - now = now - timedelta(0, 0, now.microsecond) + now = now - timedelta(microseconds=now.microsecond) + if value > now: - return "Dans le futur" + return __DATE_FMT_FUTUR else: delta = now - value # Natural time for today, absolute date after. # Reverse if in tooltip if (delta.days == 0) != tooltip: return naturaltime(value) - elif small: - return date(value, 'd/m/y à H\hi') else: - return date(value, 'l d F Y à H\hi') + return date(value, __ABS_DATE_FMT_SMALL if small else __ABS_DATE_FMT_NORMAL) @register.filter def format_date(value, small=False): - return date_formatter(value, False, small) + """Format a date to an human readable string.""" + return date_formatter(value, tooltip=False, small=small) @register.filter def tooltip_date(value): - return date_formatter(value, True, False) + """Format a date to an human readable string. To be used in tooltip.""" + return date_formatter(value, tooltip=True, small=False) + + +@register.filter('humane_time') +def humane_time(t): + """Render time (number of second from epoch) to an human readable string""" + tp = time.localtime(t) + return time.strftime(__ABS_HUMAN_TIME_FMT, tp) diff --git a/zds/utils/templatetags/emarkdown.py b/zds/utils/templatetags/emarkdown.py index 5f09b88af8..0273941cee 100644 --- a/zds/utils/templatetags/emarkdown.py +++ b/zds/utils/templatetags/emarkdown.py @@ -1,24 +1,39 @@ # coding: utf-8 +import re + from django import template from django.utils.safestring import mark_safe -import time -import re import markdown - from markdown.extensions.zds import ZdsExtension from zds.utils.templatetags.smileysDef import smileys +register = template.Library() + +""" +Markdown related filters. +""" + +# Constant strings +__MD_ERROR_PARSING = u"Une erreur est survenue dans la génération de texte Markdown. " \ + u"Veuillez rapporter le bug." -# Markdowns customs extensions : -def get_markdown_instance(Inline=False): - zdsext = ZdsExtension({"inline": Inline, "emoticons": smileys}) + +def get_markdown_instance(inline=False): + """ + Provide a pre-configured markdown parser. + + :param bool inline: If `True`, configure parser to parse only inline content. + :return: A ZMarkdown parser. + """ + zdsext = ZdsExtension({"inline": inline, "emoticons": smileys}) # Generate parser md = markdown.Markdown(extensions=(zdsext,), safe_mode = 'escape', - inline = Inline, # Protect use of html by escape it + inline = inline, + # Parse only inline content. enable_attributes = False, # Disable the conversion of attributes. # This could potentially allow an @@ -27,7 +42,8 @@ def get_markdown_instance(Inline=False): tab_length = 4, # Length of tabs in the source. # This is the default value - output_format = 'html5', # html5 output + output_format = 'html5', + # html5 output # This is the default value smart_emphasis = True, # Enable smart emphasis for underscore syntax @@ -36,75 +52,87 @@ def get_markdown_instance(Inline=False): ) return md -register = template.Library() +def render_markdown(text, inline=False): + """ + Render a markdown text to html. -@register.filter('humane_time') -def humane_time(t, conf={}): - tp = time.localtime(t) - return time.strftime("%d %b %Y, %H:%M:%S", tp) + :param str text: Text to render. + :param bool inline: If `True`, parse only inline content. + :return: Equivalent html string. + :rtype: str + """ + return get_markdown_instance(inline=inline).convert(text).encode('utf-8').strip() @register.filter(needs_autoescape=False) def emarkdown(text): + """ + Filter markdown text and render it to html. + + :param str text: Text to render. + :return: Equivalent html string. + :rtype: str + """ try: - return mark_safe( - get_markdown_instance( - Inline=False).convert(text).encode('utf-8')) + return mark_safe(render_markdown(text, inline=False)) except: - return mark_safe(u'

    Une erreur est survenue dans la génération de texte Markdown. Veuillez rapporter le bug

    ') + return mark_safe(u'

    {}

    '.format(__MD_ERROR_PARSING)) @register.filter(needs_autoescape=False) def emarkdown_inline(text): - return mark_safe( - get_markdown_instance( - Inline=True).convert(text).encode('utf-8').strip()) + """ + Filter markdown text and render it to html. Only inline elements will be parsed. + :param str text: Text to render. + :return: Equivalent html string. + :rtype: str + """ -def sub_hd1(g): - lvl = g.group('level') - hd = g.group('header') - next = "#" + lvl + hd + try: + return mark_safe(render_markdown(text, inline=True)) + except: + return mark_safe(u'

    {}

    '.format(__MD_ERROR_PARSING)) - return next +def sub_hd(match, count): + """Replace header shifted.""" + st = match.group(1) + lvl = match.group('level') + hd = match.group('header') + end = match.group(4) -def sub_hd2(g): - lvl = g.group('level') - hd = g.group('header') - next = "#" + lvl + hd + new_content = st + "#" * count + lvl + hd + end - return next + return new_content -def sub_hd3(g): - lvl = g.group('level') - hd = g.group('header') - next = "###" + lvl + hd +def decale_header(text, count): + """ + Shift header in markdown document. - return next + :param str text: Text to filter. + :param int count: + :return: Filtered text. + :rtype: str + """ + return re.sub( + r'(^|\n)(?P#{1,4})(?P
    .*?)#*(\n|$)', + lambda t: sub_hd(t, count), + text.encode("utf-8")) @register.filter('decale_header_1') def decale_header_1(text): - return re.sub( - r'(^|\n)(?P#{1,4})(?P
    .*?)#*(\n|$)', - sub_hd1, - text.encode("utf-8")) + return decale_header(text, 1) @register.filter('decale_header_2') def decale_header_2(text): - return re.sub( - r'(^|\n)(?P#{1,4})(?P
    .*?)#*(\n|$)', - sub_hd2, - text.encode("utf-8")) + return decale_header(text, 2) @register.filter('decale_header_3') def decale_header_3(text): - return re.sub( - r'(^|\n)(?P#{1,4})(?P
    .*?)#*(\n|$)', - sub_hd3, - text.encode("utf-8")) + return decale_header(text, 3) diff --git a/zds/utils/templatetags/forum.py b/zds/utils/templatetags/forum.py deleted file mode 100644 index 3f5598f156..0000000000 --- a/zds/utils/templatetags/forum.py +++ /dev/null @@ -1,10 +0,0 @@ -# coding: utf-8 - -from django import template - -register = template.Library() - - -@register.filter('readable') -def readable(forum, user): - return forum.can_read(user) diff --git a/zds/utils/templatetags/htmltotext.py b/zds/utils/templatetags/htmltotext.py deleted file mode 100644 index 057c6d897c..0000000000 --- a/zds/utils/templatetags/htmltotext.py +++ /dev/null @@ -1,20 +0,0 @@ -from django import template -import re - -register = template.Library() - - -@register.filter(needs_autoescape=False) -def htmltotext(input): - # remove the newlines - input = input.replace("\n", " ") - input = input.replace("\r", " ") - - # replace consecutive spaces into a single one - input = " ".join(input.split()) - - # remove all the tags - p = re.compile(r'<[^<]*?>') - input = p.sub('', input) - - return input \ No newline at end of file diff --git a/zds/utils/templatetags/interventions.py b/zds/utils/templatetags/interventions.py index 654ddaccc1..4bab1b8d68 100644 --- a/zds/utils/templatetags/interventions.py +++ b/zds/utils/templatetags/interventions.py @@ -6,12 +6,11 @@ from django import template from django.db.models import Q, F -from zds.article.models import never_read as never_read_article, Validation as ArticleValidation, Reaction, Article, ArticleRead -from zds.forum.models import TopicFollowed, never_read as never_read_topic, Post, Topic, TopicRead -from zds.mp.models import PrivateTopic, never_privateread, PrivateTopicRead -from zds.tutorial.models import never_read as never_read_tutorial, Validation as TutoValidation, Note, Tutorial, TutorialRead +from zds.article.models import Reaction, ArticleRead +from zds.forum.models import TopicFollowed, never_read as never_read_topic, Post, TopicRead +from zds.mp.models import PrivateTopic, PrivateTopicRead +from zds.tutorial.models import Note, TutorialRead from zds.utils.models import Alert -import collections register = template.Library() @@ -24,13 +23,15 @@ def is_read(topic): else: return True + @register.filter('humane_delta') def humane_delta(value): # mapping between label day and key - const = {1:"Aujourd'hui", 2:"Hier", 3:"Cette semaine", 4:"Ce mois-ci", 5: "Cette année"} + const = {1: "Aujourd'hui", 2: "Hier", 3: "Cette semaine", 4: "Ce mois-ci", 5: "Cette année"} return const[value] + @register.filter('followed_topics') def followed_topics(user): topicsfollowed = TopicFollowed.objects.select_related("topic").filter(user=user)\ @@ -44,16 +45,17 @@ def followed_topics(user): topics = {} for tf in topicsfollowed: for p in period: - if tf.topic.last_message.pubdate.date() >= (datetime.now() - timedelta(days=int(p[1]),\ - hours=0, minutes=0,\ - seconds=0)).date(): - if topics.has_key(p[0]): + if tf.topic.last_message.pubdate.date() >= (datetime.now() - timedelta(days=int(p[1]), + hours=0, minutes=0, + seconds=0)).date(): + if p[0] in topics: topics[p[0]].append(tf.topic) else: - topics[p[0]]= [tf.topic] + topics[p[0]] = [tf.topic] break return topics + def comp(d1, d2): v1 = int(time.mktime(d1['pubdate'].timetuple())) v2 = int(time.mktime(d2['pubdate'].timetuple())) @@ -64,69 +66,79 @@ def comp(d1, d2): else: return 0 + @register.filter('interventions_topics') def interventions_topics(user): topicsfollowed = TopicFollowed.objects.filter(user=user).values("topic").distinct().all() - + topics_never_read = TopicRead.objects\ .filter(user=user)\ - .filter(topic__in = topicsfollowed)\ + .filter(topic__in=topicsfollowed)\ .select_related("topic")\ .exclude(post=F('topic__last_message')) articlesfollowed = Reaction.objects\ - .filter(author=user)\ - .values('article')\ - .distinct().all() + .filter(author=user)\ + .values('article')\ + .distinct().all() articles_never_read = ArticleRead.objects\ .filter(user=user)\ - .filter(article__in = articlesfollowed)\ + .filter(article__in=articlesfollowed)\ .select_related("article")\ .exclude(reaction=F('article__last_reaction')) tutorialsfollowed = Note.objects\ - .filter(author=user)\ - .values('tutorial')\ - .distinct().all() + .filter(author=user)\ + .values('tutorial')\ + .distinct().all() tutorials_never_read = TutorialRead.objects\ .filter(user=user)\ - .filter(tutorial__in = tutorialsfollowed)\ + .filter(tutorial__in=tutorialsfollowed)\ .exclude(note=F('tutorial__last_note')) - + posts_unread = [] for art in articles_never_read: content = art.article.first_unread_reaction() - posts_unread.append({'pubdate':content.pubdate, 'author':content.author, 'title':art.article.title, 'url':content.get_absolute_url()}) - + posts_unread.append({'pubdate': content.pubdate, + 'author': content.author, + 'title': art.article.title, + 'url': content.get_absolute_url()}) + for tuto in tutorials_never_read: content = tuto.tutorial.first_unread_note() - posts_unread.append({'pubdate':content.pubdate, 'author':content.author, 'title':tuto.tutorial.title, 'url':content.get_absolute_url()}) + posts_unread.append({'pubdate': content.pubdate, + 'author': content.author, + 'title': tuto.tutorial.title, + 'url': content.get_absolute_url()}) for top in topics_never_read: content = top.topic.first_unread_post() if content is None: content = top.topic.last_message - posts_unread.append({'pubdate':content.pubdate, 'author':content.author, 'title':top.topic.title, 'url':content.get_absolute_url()}) + posts_unread.append({'pubdate': content.pubdate, + 'author': content.author, + 'title': top.topic.title, + 'url': content.get_absolute_url()}) - posts_unread.sort(cmp = comp) + posts_unread.sort(cmp=comp) return posts_unread @register.filter('interventions_privatetopics') def interventions_privatetopics(user): - - topics_never_read = list(PrivateTopicRead.objects\ - .filter(user=user)\ - .filter(privatepost=F('privatetopic__last_message')).all()) - + + topics_never_read = list(PrivateTopicRead.objects + .filter(user=user) + .filter(privatepost=F('privatetopic__last_message')).all()) + tnrs = [] for tnr in topics_never_read: tnrs.append(tnr.privatetopic.pk) - + privatetopics_unread = PrivateTopic.objects\ .filter(Q(author=user) | Q(participants__in=[user]))\ .exclude(pk__in=tnrs)\ @@ -137,61 +149,6 @@ def interventions_privatetopics(user): return {'unread': privatetopics_unread} -@register.simple_tag(name='reads_topic') -def reads_topic(topic, user): - if user.is_authenticated(): - if never_read_topic(topic, user): - return '' - else: - return 'secondary' - else: - return '' - - -@register.simple_tag(name='reads_article') -def reads_article(article, user): - if user.is_authenticated(): - if never_read_article(article, user): - return '' - else: - return 'secondary' - else: - return '' - - -@register.simple_tag(name='reads_tutorial') -def reads_tutorial(tutorial, user): - if user.is_authenticated(): - if never_read_tutorial(tutorial, user): - return '' - else: - return 'secondary' - else: - return '' - - -@register.filter(name='alerts_validation_tutos') -def alerts_validation_tutos(user): - tutos = TutoValidation.objects.order_by('-date_proposition').all() - total = [] - for tuto in tutos: - if tuto.is_pending(): - total.append(tuto) - - return {'total': len(total), 'alert': total[:5]} - - -@register.filter(name='alerts_validation_articles') -def alerts_validation_articles(user): - articles = ArticleValidation.objects.order_by('-date_proposition').all() - total = [] - for article in articles: - if article.is_pending(): - total.append(article) - - return {'total': len(total), 'alert': total[:5]} - - @register.filter(name='alerts_list') def alerts_list(user): total = [] diff --git a/zds/utils/templatetags/model_name.py b/zds/utils/templatetags/model_name.py index 3b1cb93cbc..94050bae10 100644 --- a/zds/utils/templatetags/model_name.py +++ b/zds/utils/templatetags/model_name.py @@ -1,8 +1,9 @@ from django import template from zds.search.constants import MODEL_NAMES - + register = template.Library() - + + @register.tag(name="model_name") def do_model_name(parser, token): try: @@ -10,17 +11,20 @@ def do_model_name(parser, token): except ValueError: raise template.TemplateSyntaxError("%r tag requires three arguments" % token.contents.split()[0]) return ModelNameNode(app_label, model_name, plural) - + + class ModelNameNode(template.Node): + def __init__(self, app_label, model_name, plural): self.app_label = template.Variable(app_label) self.model_name = template.Variable(model_name) self.plural = template.Variable(plural) + def render(self, context): try: - app_label = self.app_label.resolve(context) - model_name = self.model_name.resolve(context) - plural = self.plural.resolve(context) - return MODEL_NAMES[app_label][model_name][plural]; + app_label = self.app_label.resolve(context) + model_name = self.model_name.resolve(context) + plural = self.plural.resolve(context) + return MODEL_NAMES[app_label][model_name][plural] except template.VariableDoesNotExist: - return '' \ No newline at end of file + return '' diff --git a/zds/utils/templatetags/profile.py b/zds/utils/templatetags/profile.py index e7cf17cc44..33af5d904d 100644 --- a/zds/utils/templatetags/profile.py +++ b/zds/utils/templatetags/profile.py @@ -29,14 +29,6 @@ def user(pk): return user -@register.filter('mode') -def mode(mode): - if mode == 'W': - return 'pencil' - else: - return 'eye' - - @register.filter('state') def state(user): try: diff --git a/zds/utils/templatetags/repo_reader.py b/zds/utils/templatetags/repo_reader.py index be515b862b..26c02fe5e5 100644 --- a/zds/utils/templatetags/repo_reader.py +++ b/zds/utils/templatetags/repo_reader.py @@ -3,89 +3,10 @@ from difflib import HtmlDiff from django import template -from git import * - -from zds.utils import slugify - register = template.Library() -@register.filter('repo_tuto') -def repo_tuto(tutorial, sha=None): - if sha is None: - - return {'introduction': tutorial.get_introduction(), - 'conclusion': tutorial.get_conclusion()} - else: - repo = Repo(tutorial.get_path()) - bls = repo.commit(sha).tree.blobs - for bl in bls: - if bl.path == 'introduction.md': - intro = bl.data_stream.read() - if bl.path == 'conclusion.md': - conclu = bl.data_stream.read() - - return { - 'introduction': intro.decode('utf-8'), - 'conclusion': conclu.decode('utf-8')} - - -@register.filter('repo_part') -def repo_part(part, sha=None): - if sha is None: - return {'introduction': part.get_introduction(), - 'conclusion': part.get_conclusion()} - else: - repo = Repo(part['path']) - bls = repo.commit(sha).tree.blobs - for bl in bls: - if bl.path == 'introduction.md': - intro = bl.data_stream.read() - if bl.path == 'conclusion.md': - conclu = bl.data_stream.read() - - return { - 'introduction': intro.decode('utf-8'), - 'conclusion': conclu.decode('utf-8')} - - -@register.filter('repo_chapter') -def repo_chapter(chapter, sha=None): - if sha is None: - return {'introduction': chapter.get_introduction(), - 'conclusion': chapter.get_conclusion()} - else: - repo = Repo(chapter['path']) - if chapter['type'] == 'MINI': - return {'introduction': None, 'conclusion': None} - else: - bls = repo.commit(sha).tree.blobs - for bl in bls: - if bl.path == 'introduction.md': - intro = bl.data_stream.read() - if bl.path == 'conclusion.md': - conclu = bl.data_stream.read() - return { - 'introduction': intro.decode('utf-8'), - 'conclusion': conclu.decode('utf-8')} - - -@register.filter('repo_extract') -def repo_extract(extract, sha=None): - if sha is None: - return {'text': extract.get_text()} - else: - repo_e = Repo(extract['path']) - bls_e = repo_e.commit(sha).tree.blobs - - for bl in bls_e: - if bl.path == slugify(extract['title']) + '.md': - text = bl.data_stream.read() - return {'text': text.decode('utf-8')} - return {'text': ''} - - @register.filter('repo_blob') def repo_blob(blob): contenu = blob.data_stream.read() diff --git a/zds/utils/templatetags/set.py b/zds/utils/templatetags/set.py index b07964aedc..9dd7f760f0 100644 --- a/zds/utils/templatetags/set.py +++ b/zds/utils/templatetags/set.py @@ -4,6 +4,7 @@ class SetVarNode(template.Node): + def __init__(self, var_name, var_value): self.var_name = var_name self.var_value = var_value @@ -22,4 +23,4 @@ def set(parser, token): parts = token.split_contents() if len(parts) < 4: raise template.TemplateSyntaxError("'set' tag must be of the form: {% set as %}") - return SetVarNode(parts[3], parts[1]) \ No newline at end of file + return SetVarNode(parts[3], parts[1]) diff --git a/zds/utils/templatetags/smileysDef.py b/zds/utils/templatetags/smileysDef.py index 9c84bc24df..0f83212704 100644 --- a/zds/utils/templatetags/smileysDef.py +++ b/zds/utils/templatetags/smileysDef.py @@ -1,13 +1,9 @@ # coding: utf-8 import os +from zds import settings - -if __name__ == "__main__": - smileys_baseURL = os.path.join("../../../assets", "smileys") -else: - from zds import settings - smileys_baseURL = os.path.join(settings.STATIC_URL, "smileys") +smileys_baseURL = os.path.join(settings.STATIC_URL, "smileys") smileys_base = { "smile.png": (":)", ":-)", ), @@ -41,8 +37,3 @@ for imageFile, symboles in smileys_base.iteritems(): for symbole in symboles: smileys[symbole] = os.path.join(smileys_baseURL, imageFile) - -if __name__ == "__main__": - for image in smileys.values(): - if not os.path.isfile(image): - print "[" + image + "] is not found !!!" diff --git a/zds/utils/templatetags/tests/__init__.py b/zds/utils/templatetags/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/utils/templatetags/tests/test_emarkdown.py b/zds/utils/templatetags/tests/test_emarkdown.py new file mode 100644 index 0000000000..1673379ef6 --- /dev/null +++ b/zds/utils/templatetags/tests/test_emarkdown.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from django.test import TestCase +from django.template import Context, Template + + +class EMarkdownTest(TestCase): + def setUp(self): + content = u"# Titre 1\n\n## Titre **2**\n\n### Titre 3\n\n> test" + self.context = Context({"content": content}) + + def test_emarkdown(self): + # The goal is not to test zmarkdown but test that template tag correctly call it + + tr = Template("{% load emarkdown %}{{ content | emarkdown}}").render(self.context) + + self.assertEqual(u"

    Titre 1

    \n" + "

    Titre 2

    \n" + "
    Titre 3
    \n" + "
    \n" + "

    test

    \n" + "
    ", tr) + + # Todo: Found a way to force parsing crash or simulate it. + + def test_emarkdown_inline(self): + # The goal is not to test zmarkdown but test that template tag correctly call it + + tr = Template("{% load emarkdown %}{{ content | emarkdown_inline}}").render(self.context) + + self.assertEqual(u"

    # Titre 1\n\n" + "## Titre 2\n\n" + "### Titre 3\n\n" + "> test\n" + "

    ", tr) + + # Todo: Found a way to force parsing crash or simulate it. + + def test_decale_header(self): + tr = Template("{% load emarkdown %}{{ content | decale_header_1}}").render(self.context) + self.assertEqual(u"## Titre 1\n\n" + "### Titre **2**\n\n" + "#### Titre 3\n\n" + "> test", tr) + + tr = Template("{% load emarkdown %}{{ content | decale_header_2}}").render(self.context) + self.assertEqual(u"### Titre 1\n\n" + "#### Titre **2**\n\n" + "##### Titre 3\n\n" + "> test", tr) + + tr = Template("{% load emarkdown %}{{ content | decale_header_3}}").render(self.context) + self.assertEqual(u"#### Titre 1\n\n" + "##### Titre **2**\n\n" + "###### Titre 3\n\n" + "> test", tr) diff --git a/zds/utils/templatetags/tests/tests_append_to_get.py b/zds/utils/templatetags/tests/tests_append_to_get.py new file mode 100644 index 0000000000..565ad15e5f --- /dev/null +++ b/zds/utils/templatetags/tests/tests_append_to_get.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + + +from django.test import TestCase, RequestFactory +from django.template import TemplateSyntaxError, Token, TOKEN_TEXT, Context, VariableDoesNotExist, Template + +from zds.utils.templatetags.append_to_get import easy_tag, AppendGetNode + + +class EasyTagTest(TestCase): + def setUp(self): + def my_function(a, b, c): + """My doc string.""" + return a, b, c + + self.simple_function = my_function + self.wrapped_function = easy_tag(self.simple_function) + + def test_valid_call(self): + + # Call tag without parser and three elements + ret = self.wrapped_function(None, Token(TOKEN_TEXT, "elem1 elem2 elem3")) + + # Check arguments have been split + self.assertEqual(3, len(ret)) + self.assertEqual("elem1", ret[0]) + self.assertEqual("elem2", ret[1]) + self.assertEqual("elem3", ret[2]) + + # Check functions wrapping + self.assertEqual(self.simple_function.__name__, self.wrapped_function.__name__) + self.assertEqual(self.simple_function.__doc__, self.wrapped_function.__doc__) + + def test_invalid_call(self): + + wf = self.wrapped_function + # Check raising TemplateSyntaxError if call with too few arguments + self.assertRaises(TemplateSyntaxError, wf, None, Token(TOKEN_TEXT, "elem1 elem2")) + + # Check raising TemplateSyntaxError if call with too many arguments + self.assertRaises(TemplateSyntaxError, wf, None, Token(TOKEN_TEXT, "elem1 elem2 elem3 elem4")) + + +class AppendGetNodeTest(TestCase): + def setUp(self): + # Every test needs access to the request factory. + factory = RequestFactory() + + self.context = Context({'request': factory.get('/data/test'), 'var1': 1, 'var2': 2}) + + def test_valid_call(self): + + # Test normal call + agn = AppendGetNode("key1=var1,key2=var2") + tr = agn.render(self.context) + self.assertTrue(tr == "/data/test?key1=1&key2=2" or tr == "/data/test?key2=2&key1=1") + + # Test call with one argument + agn = AppendGetNode("key1=var1") + tr = agn.render(self.context) + self.assertEqual(tr, "/data/test?key1=1") + + # Test call without arguments + agn = AppendGetNode("") + tr = agn.render(self.context) + self.assertEqual(tr, "/data/test") + + def test_invalid_call(self): + + # Test invalid format + + # Space separators args : + self.assertRaises(TemplateSyntaxError, AppendGetNode, "key1=var1 key2=var2") + # No values : + self.assertRaises(TemplateSyntaxError, AppendGetNode, "key1=,key2=var2") + self.assertRaises(TemplateSyntaxError, AppendGetNode, "key1,key2=var2") + # Not resolvable variable + agn = AppendGetNode("key1=var3,key2=var2") + self.assertRaises(VariableDoesNotExist, agn.render, self.context) + + def test_valid_templatetag(self): + + # Test normal call + tr = Template("{% load append_to_get %}" + "{% append_to_get key1=var1,key2=var2 %}" + ).render(self.context) + self.assertTrue(tr == "/data/test?key1=1&key2=2" or tr == "/data/test?key2=2&key1=1") + + # Test call with one argument + tr = Template("{% load append_to_get %}" + "{% append_to_get key1=var1 %}" + ).render(self.context) + self.assertEqual(tr, "/data/test?key1=1") + + def test_invalid_templatetag(self): + # Test invalid format + + # Space separators args : + str_tp = ("{% load append_to_get %}" + "{% append_to_get key1=var1 key2=var2 %}") + self.assertRaises(TemplateSyntaxError, Template, str_tp) + + # No values : + str_tp = ("{% load append_to_get %}" + "{% append_to_get key1=,key2=var2 %}") + self.assertRaises(TemplateSyntaxError, Template, str_tp) + str_tp = ("{% load append_to_get %}" + "{% append_to_get key1,key2=var2 %}") + self.assertRaises(TemplateSyntaxError, Template, str_tp) + + # Not resolvable variable + tr = Template("{% load append_to_get %}" + "{% append_to_get key1=var3,key2=var2 %}" + ) + self.assertRaises(VariableDoesNotExist, tr.render, self.context) diff --git a/zds/utils/templatetags/tests/tests_captureas.py b/zds/utils/templatetags/tests/tests_captureas.py new file mode 100644 index 0000000000..42045b15b2 --- /dev/null +++ b/zds/utils/templatetags/tests/tests_captureas.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +from django.test import TestCase +from django.template import Context, Template, TemplateSyntaxError + + +class CaptureasNodeTest(TestCase): + def setUp(self): + self.context = Context() + + def test_valid_templatetag(self): + + # Test empty element + self.assertFalse("var1" in self.context) + tr = Template("{% load captureas %}" + "{% captureas var1%}" + "{% endcaptureas %}" + ).render(self.context) + self.assertTrue("var1" in self.context) + + self.assertEqual(tr, "") + self.assertEqual(self.context["var1"], "") + + # Test simple content + self.assertFalse("var2" in self.context) + tr = Template("{% load captureas %}" + "{% captureas var2%}" + "{% for i in 'xxxxxxxxxx' %}" + "{{forloop.counter0}}" + "{% endfor %}" + "{% endcaptureas %}" + ).render(self.context) + self.assertTrue("var2" in self.context) + + self.assertEqual(tr, "") + self.assertEqual(self.context["var2"], "0123456789") + + def test_invalid_templatetag(self): + # No var name + tp = ("{% load captureas %}" + "{% captureas%}" + "{% endcaptureas %}" + ) + self.assertRaises(TemplateSyntaxError, Template, tp) + + # Too many var name + tp = ("{% load captureas %}" + "{% captureas v1 v2%}" + "{% endcaptureas %}" + ) + self.assertRaises(TemplateSyntaxError, Template, tp) diff --git a/zds/utils/templatetags/tests/tests_date.py b/zds/utils/templatetags/tests/tests_date.py new file mode 100644 index 0000000000..e7ed653b93 --- /dev/null +++ b/zds/utils/templatetags/tests/tests_date.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta + +from django.test import TestCase +from django.template import Context, Template + + +class DateFormatterTest(TestCase): + # todo: Add test with localization parameters + + def setUp(self): + + now = datetime.now() + date_previous_in_day = now - timedelta(hours=1) + date_previous_abs = datetime(2013, 9, 12, hour=11, minute=10, second=42, microsecond=10) + date_future_in_day = now + timedelta(hours=1) + + self.context = Context({"date_previous_in_day": date_previous_in_day, + "date_previous_abs": date_previous_abs, + "date_future_in_day": date_future_in_day, + "date_epoch": 42, + "NoneVal": None}) + + def test_format_date(self): + # Default behaviour + tr = Template("{% load date %}" + "{{ date_previous_in_day | format_date}}" + ).render(self.context) + self.assertEqual(u"il y a une heure", tr) + + tr = Template("{% load date %}" + "{{ date_future_in_day | format_date}}" + ).render(self.context) + self.assertEqual(u"Dans le futur", tr) + + tr = Template("{% load date %}" + "{{ date_previous_abs | format_date}}" + ).render(self.context) + + self.assertEqual(u"jeudi 12 septembre 2013 à 11h10", tr) + + # small == False :=> Same behaviour + tr = Template("{% load date %}" + "{{ date_previous_in_day | format_date:False}}" + ).render(self.context) + self.assertEqual(u"il y a une heure", tr) + + tr = Template("{% load date %}" + "{{ date_future_in_day | format_date:False}}" + ).render(self.context) + self.assertEqual(u"Dans le futur", tr) + + tr = Template("{% load date %}" + "{{ date_previous_abs | format_date:False}}" + ).render(self.context) + + self.assertEqual(u"jeudi 12 septembre 2013 à 11h10", tr) + + # small == True :=> absolute date change + tr = Template("{% load date %}" + "{{ date_previous_in_day | format_date:True}}" + ).render(self.context) + self.assertEqual(u"il y a une heure", tr) + + tr = Template("{% load date %}" + "{{ date_future_in_day | format_date:True}}" + ).render(self.context) + self.assertEqual(u"Dans le futur", tr) + + tr = Template("{% load date %}" + "{{ date_previous_abs | format_date:True}}" + ).render(self.context) + + self.assertEqual(u"12/09/13 à 11h10", tr) + + # Bad format + tr = Template("{% load date %}" + "{{ NoneVal | format_date}}" + ).render(self.context) + + self.assertEqual(u"None", tr) + + def test_tooltip_date(self): + # Default behaviour + + # Todo: Add test to step time less than one day with tooltip + # Todo: I don't know how to test this without hugly hack on datetime.now() + + tr = Template("{% load date %}" + "{{ date_future_in_day | tooltip_date}}" + ).render(self.context) + self.assertEqual(u"Dans le futur", tr) + + tr = Template("{% load date %}" + "{{ date_previous_abs | tooltip_date}}" + ).render(self.context) + + self.assertEqual(u"il y a 1\xa0année", tr) + + # Bad format + tr = Template("{% load date %}" + "{{ NoneVal | tooltip_date}}" + ).render(self.context) + + self.assertEqual(u"None", tr) + + def test_humane_time(self): + + # Default behaviour + tr = Template("{% load date %}" + "{{ date_epoch | humane_time}}" + ).render(self.context) + self.assertEqual(u"01 Jan 1970, 01:00:42", tr) diff --git a/zds/utils/templatetags/topbar.py b/zds/utils/templatetags/topbar.py index fb8e479b2e..0391900366 100644 --- a/zds/utils/templatetags/topbar.py +++ b/zds/utils/templatetags/topbar.py @@ -1,10 +1,11 @@ # coding: utf-8 from django import template - -from zds.forum.models import Category as fCategory, Forum +from django.conf import settings +import itertools +from zds.forum.models import Forum, Topic from zds.tutorial.models import Tutorial -from zds.utils.models import Category, SubCategory, CategorySubCategory +from zds.utils.models import CategorySubCategory, Tag register = template.Library() @@ -13,43 +14,67 @@ @register.filter('top_categories') def top_categories(user): cats = {} - + forums_pub = Forum.objects.filter(group__isnull=True).select_related("category").all() if user and user.is_authenticated(): forums_prv = Forum\ - .objects\ - .filter(group__isnull=False, group__in=user.groups.all())\ - .select_related("category").all() - forums = list(forums_pub|forums_prv) - else : + .objects\ + .filter(group__isnull=False, group__in=user.groups.all())\ + .select_related("category").all() + forums = list(forums_pub | forums_prv) + else: forums = list(forums_pub) - + for forum in forums: key = forum.category.title - if cats.has_key(key): + if key in cats: cats[key].append(forum) else: cats[key] = [forum] - - return cats + + tgs = Topic.objects\ + .values('tags', 'pk')\ + .distinct()\ + .filter(forum__in=forums, tags__isnull=False) + + cts = {} + for key, group in itertools.groupby(tgs, lambda item: item["tags"]): + for thing in group: + if key in cts: + cts[key] += 1 + else: + cts[key] = 1 + + cpt = 0 + top_tag = [] + sort_list = reversed(sorted(cts.iteritems(), key=lambda k_v: (k_v[1], k_v[0]))) + for key, value in sort_list: + top_tag.append(key) + cpt += 1 + if cpt >= settings.TOP_TAG_MAX: + break + + tags = Tag.objects.filter(pk__in=top_tag) + + return {"tags": tags, "categories": cats} @register.filter('top_categories_tuto') def top_categories_tuto(user): - + cats = {} subcats_tutos = Tutorial.objects.values('subcategory').filter(sha_public__isnull=False).all() catsubcats = CategorySubCategory.objects \ - .filter(is_main=True)\ - .filter(subcategory__in=subcats_tutos)\ - .select_related('subcategory','category')\ - .all() + .filter(is_main=True)\ + .filter(subcategory__in=subcats_tutos)\ + .select_related('subcategory', 'category')\ + .all() cscs = list(catsubcats.all()) - + for csc in cscs: key = csc.category.title - if cats.has_key(key): + if key in cats: cats[key].append(csc.subcategory) else: cats[key] = [csc.subcategory] @@ -59,4 +84,3 @@ def top_categories_tuto(user): @register.filter('auth_forum') def auth_forum(forum, user): return forum.can_read(user) - diff --git a/zds/utils/tutorials.py b/zds/utils/tutorials.py index fa53615985..c32f58edba 100644 --- a/zds/utils/tutorials.py +++ b/zds/utils/tutorials.py @@ -5,11 +5,15 @@ import os from django.template import Context from django.template.loader import get_template -from git import * +from git import Repo, Actor from zds.utils import slugify +from zds.utils.models import Licence + # Export-to-dict functions + + def export_chapter(chapter, export_all=True): from zds.tutorial.models import Extract ''' @@ -263,7 +267,7 @@ def move(obj, new_pos, position_f, parent_f, children_fn): """ old_pos = getattr(obj, position_f) objects = getattr(getattr(obj, parent_f), children_fn)() - + # Check that asked new position is correct if not 1 <= new_pos <= objects.count(): raise ValueError('Can\'t move object to position {0}'.format(new_pos)) @@ -290,3 +294,208 @@ def move(obj, new_pos, position_f, parent_f, children_fn): setattr(obj, position_f, new_pos) +def check_json(data, tutorial, zip): + from zds.tutorial.models import Part, Chapter, Extract + if "title" not in data: + return (False, u"Le tutoriel que vous souhaitez importer manque de titre") + if "type" not in data: + return (False, u"Les métadonnées du tutoriel à importer ne nous permettent pas de connaître son type") + elif tutorial.is_mini(): + if data["type"] == "BIG": + return (False, u"Vous essayez d'importer un big tutoriel dans un mini tutoriel") + elif "chapter" not in data: + return (False, u"La structure de vos métadonnées est incohérente") + elif "extracts" not in data["chapter"]: + return (False, u"La structure de vos extraits est incohérente") + else: + for extract in data["chapter"]["extracts"]: + if "pk" not in extract or "title" not in extract or "text" not in extract: + return (False, u"Un de vos extraits est mal renseigné") + elif not Extract.objects.filter(pk=extract["pk"]).exists(): + return (False, u"L'extrait « {} » n'existe pas dans notre base".format(extract["title"])) + elif not Extract.objects.filter(pk=extract["pk"], chapter__tutorial__pk=tutorial.pk).exists(): + return (False, u"Vous n'êtes pas autorisé à modifier l'extrait « {} »".format(extract["title"])) + try: + zip.getinfo(extract["text"]) + except KeyError: + return (False, + u'Le fichier « {} » renseigné dans vos métadonnées ' + u'pour l\'extrait « {} » ne se trouve pas dans votre zip'.format( + extract["text"], + extract["title"])) + subs = ["introduction", "conclusion"] + for sub in subs: + if sub in data: + try: + zip.getinfo(data[sub]) + except KeyError: + return (False, + u'Le fichier « {} » renseigné dans vos métadonnées ' + u'pour le tutoriel « {} » ne se trouve pas dans votre zip'.format( + data[sub], data["title"])) + elif tutorial.is_big(): + if data["type"] == "MINI": + return (False, u"Vous essayez d'importer un mini tutoriel dans un big tutoriel") + elif "parts" not in data: + return (False, u"La structure de vos métadonnées est incohérente") + else: + for part in data["parts"]: + if "pk" not in part or "title" not in part: + return (False, u"La structure de vos parties est incohérente") + elif not Part.objects.filter(pk=part["pk"]).exists(): + return (False, u"La partie « {} » n'existe pas dans notre base".format( + part["title"])) + elif not Part.objects.filter(pk=part["pk"], tutorial__pk=tutorial.pk).exists(): + return (False, u"La partie « {} » n'est pas dans le tutoriel à modifier ".format( + part["title"])) + if "chapters" in part: + for chapter in part["chapters"]: + if "pk" not in chapter or "title" not in chapter: + return (False, u"La structure de vos chapitres est incohérente") + elif not Chapter.objects.filter(pk=chapter["pk"]).exists(): + return (False, u"Le chapitre « {} » n'existe pas dans notre base".format(chapter["title"])) + elif not Chapter.objects.filter(pk=chapter["pk"], part__tutorial__pk=tutorial.pk).exists(): + return (False, u"Le chapitre « {} » n'est pas dans le tutoriel a modifier".format( + chapter["title"])) + elif "extracts" in chapter: + for extract in chapter["extracts"]: + if "pk" not in extract or "title" not in extract or "text" not in extract: + return (False, u"Un de vos extraits est mal renseigné") + elif not Extract.objects.filter(pk=extract["pk"]).exists(): + return (False, u"L'extrait « {} » n'existe pas dans notre base".format( + extract["title"])) + elif not Extract.objects.filter(pk=extract["pk"], + chapter__part__tutorial__pk=tutorial.pk).exists(): + return (False, u"Vous n'êtes pas autorisé à modifier l'extrait « {} » ".format( + extract["title"])) + try: + zip.getinfo(extract["text"]) + except KeyError: + return (False, u'Le fichier « {} » renseigné dans vos métadonnées ' + u'pour l\'extrait « {} » ne se trouve pas dans votre zip'. + format(extract["text"], extract["title"])) + subs = ["introduction", "conclusion"] + for sub in subs: + if sub in chapter: + try: + zip.getinfo(chapter[sub]) + except KeyError: + return (False, u'Le fichier « {} » renseigné dans vos métadonnées ' + u'pour le chapitre « {} » ne se trouve pas dans votre zip' + .format(chapter[sub], chapter["title"])) + subs = ["introduction", "conclusion"] + for sub in subs: + if sub in part: + try: + zip.getinfo(part[sub]) + except KeyError: + return (False, + u'Le fichier « {} » renseigné dans vos métadonnées ' + u'pour la partie « {} » ne se trouve pas dans votre zip'.format( + part[sub], part["title"])) + subs = ["introduction", "conclusion"] + for sub in subs: + if sub in data: + try: + zip.getinfo(data[sub]) + except KeyError: + return (False, + u'Le fichier « {} » renseigné dans vos métadonnées ' + u'pour le tutoriel « {} » ne se trouve pas dans votre zip'.format( + data[sub], data["title"])) + return (True, None) + + +def import_archive(request): + from zds.tutorial.models import Tutorial + import zipfile + import shutil + import os + try: + import ujson as json_reader + except: + try: + import simplejson as json_reader + except: + import json as json_reader + + archive = request.FILES["file"] + tutorial = Tutorial.objects.get(pk=request.POST["tutorial"]) + ext = str(archive).split(".")[-1] + if ext == "zip": + zfile = zipfile.ZipFile(archive, "a") + json_here = False + for i in zfile.namelist(): + ph = i + if ph == "manifest.json": + json_data = zfile.read(i) + mandata = json_reader.loads(json_data) + ck_zip = zipfile.ZipFile(archive, "r") + (check, reason) = check_json(mandata, tutorial, ck_zip) + if not check: + return (check, reason) + tutorial.title = mandata['title'] + if "description" in mandata: + tutorial.description = mandata['description'] + if "introduction" in mandata: + tutorial.introduction = mandata['introduction'] + if "conclusion" in mandata: + tutorial.conclusion = mandata['conclusion'] + if "licence" in mandata: + tutorial.licence = Licence.objects.filter(code=mandata["licence"]).all()[0] + old_path = tutorial.get_path() + tutorial.save() + new_path = tutorial.get_path() + shutil.move(old_path, new_path) + json_here = True + break + if not json_here: + return (False, u"L'archive n'a pas pu être importée car le " + u"fichier manifest.json (fichier de métadonnées est introuvable).") + + # init git + repo = Repo(tutorial.get_path()) + index = repo.index + + # delete old file + for filename in os.listdir(tutorial.get_path()): + if not filename.startswith('.'): + mf = os.path.join(tutorial.get_path(), filename) + if os.path.isfile(mf): + os.remove(mf) + elif os.path.isdir(mf): + shutil.rmtree(mf) + # copy new file + for i in zfile.namelist(): + ph = i + if ph != "": + ph_dest = os.path.join(tutorial.get_path(), ph) + try: + data = zfile.read(i) + fp = open(ph_dest, "wb") + fp.write(data) + fp.close() + index.add([ph]) + except IOError: + try: + os.makedirs(ph_dest) + except: + pass + zfile.close() + + # save in git + msg = "Import du tutoriel" + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@zestedesavoir.com" + com = index.commit(msg.encode("utf-8"), + author=Actor(aut_user, aut_email), + committer=Actor(aut_user, aut_email)) + tutorial.sha_draft = com.hexsha + tutorial.save() + tutorial.update_children() + + return (True, u"Le tutoriel {} a été importé avec succès".format(tutorial.title)) + else: + return (False, u"L'archive n'a pas pu être importée car elle n'est pas au format zip")