diff --git a/boards/admin.py b/boards/admin.py --- a/boards/admin.py +++ b/boards/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from boards.models import Post, Tag, Admin, Ban +from boards.models import Post, Tag, User, Ban admin.site.register(Post) admin.site.register(Tag) -admin.site.register(Admin) -admin.site.register(Ban) \ No newline at end of file +admin.site.register(User) +admin.site.register(Ban) diff --git a/boards/forms.py b/boards/forms.py --- a/boards/forms.py +++ b/boards/forms.py @@ -2,7 +2,7 @@ import re from captcha.fields import CaptchaField from django import forms from django.forms.util import ErrorList -from boards.models import TITLE_MAX_LENGTH +from boards.models import TITLE_MAX_LENGTH, User from neboard import settings from boards import utils @@ -67,7 +67,6 @@ class PostForm(forms.Form): self._errors['image'] = self.error_class([error_message]) - class ThreadForm(PostForm): regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE) tags = forms.CharField(max_length=100) @@ -132,3 +131,21 @@ class ThreadCaptchaForm(ThreadForm): class SettingsForm(forms.Form): theme = forms.ChoiceField(choices=settings.THEMES, widget=forms.RadioSelect) + + +class LoginForm(forms.Form): + user_id = forms.CharField() + + def clean_user_id(self): + user_id = self.cleaned_data['user_id'] + if user_id: + users = User.objects.filter(user_id=user_id) + if len(users) == 0: + raise forms.ValidationError('No such user found') + + return user_id + + def clean(self): + cleaned_data = super(LoginForm, self).clean() + + return cleaned_data \ No newline at end of file diff --git a/boards/locale/ru/LC_MESSAGES/django.mo b/boards/locale/ru/LC_MESSAGES/django.mo index 681c3a0d87a8fd2505fc9b2f0cd2782467799575..ef9fbb2c1feece5d76d10ef2263833278e48bb41 GIT binary patch literal 3476 zc$|$@TW=f36`sVFDpQn`xNeiwu`_NHU+OGPS_bOSk`u7%N&=adB1*F4$Gpxy1_c5XC{Un3pl=2Gote?as9JPk z?l(JgF5mgi8TtEtyZ^xOoW$=L{O&x$*m+=b7gNtuyBVY9)4*2XLEsO7$AQhj4DiRm zGr%W+eP-VV{s=e*+ylG@#6NZuuO{Fv;M2hOfo;IAfX@S0&G}z}dx3uk9tHl>#5vTY z;~#6%@lFC0z|+73z?aPVZ<=(RVZ0tA9>#9m_{{)Yh>x**&+LBy>;QgbzP~f)-{bWZ z@Nee(UwAzV+|{i0Xl~Z~dx86a`+-^D(PkZIxLNZm8T5=_&EVSxuLGCRgImDI@ttqc z{4W8Mz|j`XBLtoR&H;Z4Y_w>-{?elL`iJ@6w@24~c#rPW3&3;0^T6Z4Y2dGcU+&R* z?1u3{U>nc^o&f$3csZf#su-M1X#e+t7lCUDov*1?=iA?^=jqv2owuV^`wbc#ZPocE z3{C@Y0B4Q=zNFqi2y6!)Nov2>lKQ-o)cqb$>N&cT)Vh2Pd>!~La6j-=o6di+P3!mD zHXY|mo9271P50v#@Da+x^gJG7dVc9?VO!5pgU{i|nU3`{re~Ax5Y2GX*t~$>lT7C) zZj^_#q&`1pY^aAnVOpzW+wwfVZT)lVAJg^F9ZNA?Gd<55q+U?J9^K~G#2$V~H=31Q z+xBQS>0Xl-)b|4$2RX-KxpJ9DV}Y=pkmYL8m>-1f72ETK0|?y_Ua^BKj(^qT;e;32 z<0*C?#*yHjxN00(mne%!ur4=@0(ZC;35VA_M+7(*EI)2ns%4So?1Dhh?uuO!tlJAk z5b+7W7VsLD+;zA;?7t~k!44x{t2#&lmxAkE33=pm+Y7IXAmk&q$A<-Xl|S-ab)zv> z@Jp`8dVL<*rI3&MNS;!kx-Krp!7RlMaggGpZh$ntCl13d(k`qY%)%_|_rplNxU0DS zs^&-HzhoW|6(T(#s^tm&CbC8reEXpr`N0Gmv|%+UqR911AuHM?)P}cOhP+ZkGQ@U7 z*{!$~e0YL~ReMB)DP9!gU>ayL+zojk-l(~OK#6q3iesW8@Wx4%EF2$XJrt#?W<@tD z3pP|`LnL!|m%=Q&?AN#*2ww5gCmS47v;~8HZaYpO!jO;IAxH^St;PZR`51taNC{{= z71yJFlP??g%MMcnL*_29x2@Q39FU$LL=U>fT z%=1h6fx+&+-UJC3L{?8&avkfHS}C-OewHWt`-+z8A)Kr(H6W+bnRYAvqLuyy&%BsD z-F_n7o=(Hi8W3-~;g;Ryd~U$%>9e|W{Fi;%p6(N`_4VcxG;3Bdz-*PNj4Zz}R4DM? zTu+{N1>#`9~$8${nKAw_`Jf4<| z@_szaZ6`iy7gGDnDpT!Y)ZT%mP5wfXlZ z#pLI(TapWMo)U7orrc)YcQ=yGF!==?Bah+q8EmKI14H9qE*~Jq z0+XNOq#^HPe-bxQeu!|21(SbN?CvVhZ)h8Ki>g?~vL4S-aw1-b-7;>J4!Ro8>AHw5 z{8lL&ii~fP`=`VQG#5d44ev#+)KnFzZIyudXVX<)FMS??HoU z4vw>0m-+@b9SLTTQIUdm{0v#u`A{u1E3`F_avEq0MOmR_9Bfu8Tw_N))z)nsZOr7HY4c_P zy}VD8{}t7z?&QNA)RThU8ET6X8~xYP(KWxPDp^iJF@V|xX{OsYR*b*AR)hB{lK4$%4L{UuUNPZs+(OhhMT diff --git a/boards/locale/ru/LC_MESSAGES/django.po b/boards/locale/ru/LC_MESSAGES/django.po --- a/boards/locale/ru/LC_MESSAGES/django.po +++ b/boards/locale/ru/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-08-30 18:54+0300\n" +"POT-Creation-Date: 2013-09-07 19:43+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -54,24 +54,37 @@ msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором" msgid "Feed" msgstr "Лента" -#: templates/boards/base.html:36 +#: templates/boards/base.html:29 msgid "All threads" msgstr "Все темы" -#: templates/boards/base.html:42 +#: templates/boards/base.html:35 msgid "Settings" msgstr "Настройки" -#: templates/boards/base.html:50 +#: templates/boards/base.html:42 templates/boards/login.html:6 +#: templates/boards/login.html.py:21 +msgid "Login" +msgstr "Вход" + +#: templates/boards/base.html:43 msgid "Up" msgstr "Вверх" +#: templates/boards/login.html:15 +msgid "User ID" +msgstr "ID пользователя" + +#: templates/boards/login.html:24 +msgid "Insert your user id above" +msgstr "Вставьте свой ID пользователя выше" + #: templates/boards/posting_general.html:18 msgid "Tag: " msgstr "Тег: " #: templates/boards/posting_general.html:35 -#: templates/boards/posting_general.html:81 templates/boards/thread.html:27 +#: templates/boards/posting_general.html:89 templates/boards/thread.html:27 #: templates/boards/rss/post.html:5 msgid "Post image" msgstr "Изображение сообщения" @@ -80,93 +93,124 @@ msgstr "Изображение сообщения" msgid "Reply" msgstr "Ответ" -#: templates/boards/posting_general.html:55 templates/boards/thread.html:108 +#: templates/boards/posting_general.html:54 templates/boards/thread.html:46 +msgid "Delete" +msgstr "Удалить" + +#: templates/boards/posting_general.html:63 templates/boards/thread.html:113 msgid "replies" msgstr "ответов" -#: templates/boards/posting_general.html:56 templates/boards/thread.html:109 +#: templates/boards/posting_general.html:64 templates/boards/thread.html:114 msgid "images" msgstr "изображений" -#: templates/boards/posting_general.html:58 -#: templates/boards/posting_general.html:131 templates/boards/thread.html:48 -#: templates/boards/rss/post.html:10 +#: templates/boards/posting_general.html:66 +#: templates/boards/posting_general.html:139 templates/boards/tags.html:7 +#: templates/boards/thread.html:56 templates/boards/rss/post.html:10 msgid "Tags" msgstr "Теги" -#: templates/boards/posting_general.html:113 +#: templates/boards/posting_general.html:115 +msgid "No threads exist. Create the first one!" +msgstr "Нет тем. Создайте первую!" + +#: templates/boards/posting_general.html:121 msgid "Create new thread" msgstr "Создать новую тему" -#: templates/boards/posting_general.html:116 templates/boards/thread.html:70 +#: templates/boards/posting_general.html:124 templates/boards/thread.html:75 msgid "Title" msgstr "Заголовок" -#: templates/boards/posting_general.html:121 templates/boards/thread.html:75 +#: templates/boards/posting_general.html:129 templates/boards/thread.html:80 msgid "Text" msgstr "Текст" -#: templates/boards/posting_general.html:126 templates/boards/thread.html:80 +#: templates/boards/posting_general.html:134 templates/boards/thread.html:85 msgid "Image" msgstr "Изображение" -#: templates/boards/posting_general.html:141 templates/boards/thread.html:91 +#: templates/boards/posting_general.html:149 templates/boards/thread.html:96 msgid "Post" msgstr "Отправить" -#: templates/boards/posting_general.html:143 +#: templates/boards/posting_general.html:151 msgid "Tags must be delimited by spaces. Text or image is required." msgstr "" "Теги должны быть разделены пробелами. Текст или изображение обязательны." -#: templates/boards/posting_general.html:146 templates/boards/thread.html:93 +#: templates/boards/posting_general.html:154 templates/boards/thread.html:98 msgid "Basic markdown syntax." msgstr "Базовый синтаксис markdown." -#: templates/boards/posting_general.html:156 +#: templates/boards/posting_general.html:164 msgid "Pages:" msgstr "Страницы: " -#: templates/boards/settings.html:13 +#: templates/boards/settings.html:12 +msgid "User:" +msgstr "Пользователь:" + +#: templates/boards/settings.html:14 +msgid "You are moderator." +msgstr "Вы модератор." + +#: templates/boards/settings.html:20 msgid "Theme" msgstr "Тема" -#: templates/boards/settings.html:29 +#: templates/boards/settings.html:36 msgid "Save" msgstr "Сохранить" -#: templates/boards/tags.html:7 -msgid "tags" -msgstr "тегов" +#: templates/boards/tags.html:17 +msgid "threads" +msgstr "тем" + +#: templates/boards/tags.html:20 +msgid "Remove" +msgstr "Удалить" -#: templates/boards/thread.html:67 +#: templates/boards/tags.html:23 +msgid "Add" +msgstr "Добавить" + +#: templates/boards/tags.html:28 +msgid "No tags found." +msgstr "Теги не найдены." + +#: templates/boards/thread.html:72 msgid "Reply to thread" msgstr "Ответить в тему" -#: templates/boards/thread.html:94 +#: templates/boards/thread.html:99 msgid "Example: " msgstr "Пример: " -#: templates/boards/thread.html:94 +#: templates/boards/thread.html:99 msgid "italic" msgstr "курсив" -#: templates/boards/thread.html:95 +#: templates/boards/thread.html:100 msgid "bold" msgstr "полужирный" -#: templates/boards/thread.html:96 +#: templates/boards/thread.html:101 msgid "Quotes can be inserted with" msgstr "Цитаты могут быть вставлены при помощи" -#: templates/boards/thread.html:97 +#: templates/boards/thread.html:102 msgid "Links to answers can be inserted with" msgstr "Ссылки на ответы могут быть вставлены с помощью" -#: templates/boards/thread.html:110 +#: templates/boards/thread.html:115 msgid "Last update: " msgstr "Последнее обновление: " +#~ msgid "tags" +#~ msgstr "тегов" + #~ msgid "Get!" #~ msgstr "Гет!" diff --git a/boards/migrations/0004_auto__del_admin__add_user__add_setting__add_field_post_user.py b/boards/migrations/0004_auto__del_admin__add_user__add_setting__add_field_post_user.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0004_auto__del_admin__add_user__add_setting__add_field_post_user.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting model 'Admin' + db.delete_table(u'boards_admin') + + # Adding model 'User' + db.create_table(u'boards_user', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user_id', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('rank', self.gf('django.db.models.fields.IntegerField')()), + ('registration_time', self.gf('django.db.models.fields.DateTimeField')()), + ('last_access_time', self.gf('django.db.models.fields.DateTimeField')()), + )) + db.send_create_signal(u'boards', ['User']) + + # Adding M2M table for field fav_tags on 'User' + m2m_table_name = db.shorten_name(u'boards_user_fav_tags') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm[u'boards.user'], null=False)), + ('tag', models.ForeignKey(orm[u'boards.tag'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'tag_id']) + + # Adding M2M table for field fav_threads on 'User' + m2m_table_name = db.shorten_name(u'boards_user_fav_threads') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm[u'boards.user'], null=False)), + ('post', models.ForeignKey(orm[u'boards.post'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'post_id']) + + # Adding model 'Setting' + db.create_table(u'boards_setting', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('value', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['boards.User'])), + )) + db.send_create_signal(u'boards', ['Setting']) + + # Adding field 'Post.user' + db.add_column(u'boards_post', 'user', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['boards.User'], null=True), + keep_default=False) + + + def backwards(self, orm): + # Adding model 'Admin' + db.create_table(u'boards_admin', ( + ('password', self.gf('django.db.models.fields.CharField')(max_length=100)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), + )) + db.send_create_signal(u'boards', ['Admin']) + + # Deleting model 'User' + db.delete_table(u'boards_user') + + # Removing M2M table for field fav_tags on 'User' + db.delete_table(db.shorten_name(u'boards_user_fav_tags')) + + # Removing M2M table for field fav_threads on 'User' + db.delete_table(db.shorten_name(u'boards_user_fav_threads')) + + # Deleting model 'Setting' + db.delete_table(u'boards_setting') + + # Deleting field 'Post.user' + db.delete_column(u'boards_post', 'user_id') + + + models = { + u'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}) + }, + u'boards.post': { + 'Meta': {'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'parent': ('django.db.models.fields.BigIntegerField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['boards.User']", 'null': 'True'}) + }, + u'boards.setting': { + 'Meta': {'object_name': 'Setting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['boards.User']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'boards.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'boards.user': { + 'Meta': {'object_name': 'User'}, + 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}), + 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'+'", 'symmetrical': 'False', 'to': u"orm['boards.Post']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_access_time': ('django.db.models.fields.DateTimeField', [], {}), + 'rank': ('django.db.models.fields.IntegerField', [], {}), + 'registration_time': ('django.db.models.fields.DateTimeField', [], {}), + 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['boards'] \ No newline at end of file diff --git a/boards/models.py b/boards/models.py --- a/boards/models.py +++ b/boards/models.py @@ -27,10 +27,14 @@ OPENING_POST_POPULARITY_WEIGHT = 2 IMAGES_DIRECTORY = 'images/' FILE_EXTENSION_DELIMITER = '.' +RANK_ADMIN = 0 +RANK_MODERATOR = 10 +RANK_USER = 100 + class PostManager(models.Manager): def create_post(self, title, text, image=None, parent_id=NO_PARENT, - ip=NO_IP, tags=None): + ip=NO_IP, tags=None, user=None): post = self.create(title=title, text=text, pub_time=timezone.now(), @@ -38,7 +42,8 @@ class PostManager(models.Manager): image=image, poster_ip=ip, poster_user_agent=UNKNOWN_UA, - last_edit_time=timezone.now()) + last_edit_time=timezone.now(), + user=user) if tags: map(post.tags.add, tags) @@ -224,6 +229,7 @@ class Post(models.Model): parent = models.BigIntegerField() tags = models.ManyToManyField(Tag) last_edit_time = models.DateTimeField() + user = models.ForeignKey('User', null=True, default=None) def __unicode__(self): return '#' + str(self.id) + ' ' + self.title + ' (' + \ @@ -271,19 +277,54 @@ class Post(models.Model): return last_replies -class Admin(models.Model): - """ - Model for admin users - """ - name = models.CharField(max_length=100) - password = models.CharField(max_length=100) +class User(models.Model): + + user_id = models.CharField(max_length=50) + rank = models.IntegerField() + + registration_time = models.DateTimeField() + last_access_time = models.DateTimeField() + + fav_tags = models.ManyToManyField(Tag, null=True, blank=True) + fav_threads = models.ManyToManyField(Post, related_name='+', null=True, + blank=True) + + def save_setting(self, name, value): + setting, created = Setting.objects.get_or_create(name=name, user=self) + setting.value = value + setting.save() + + return setting + + def get_setting(self, name): + settings = Setting.objects.filter(name=name, user=self) + if len(settings) > 0: + setting = settings[0] + else: + setting = None + + if setting: + setting_value = setting.value + else: + setting_value = None + + return setting_value + + def is_moderator(self): + return RANK_MODERATOR >= self.rank def __unicode__(self): - return self.name + '/' + '*' * len(self.password) + return self.user_id + + +class Setting(models.Model): + + name = models.CharField(max_length=50) + value = models.CharField(max_length=50) + user = models.ForeignKey(User) class Ban(models.Model): - ip = models.GenericIPAddressField() def __unicode__(self): diff --git a/boards/static/css/md/base_page.css b/boards/static/css/md/base_page.css --- a/boards/static/css/md/base_page.css +++ b/boards/static/css/md/base_page.css @@ -128,7 +128,7 @@ p { .form-errors { padding-left: 1ex; font-weight: bold; - vertical-align: top; + vertical-align: middle; } .post-form input, .post-form textarea { @@ -274,6 +274,11 @@ li { color: #ddd; } +.moderator_info { + color: #e99d41; + float: right; +} + .refmap { font-size: 0.9em; color: #ccc; diff --git a/boards/static/css/sw/base_page.css b/boards/static/css/sw/base_page.css --- a/boards/static/css/sw/base_page.css +++ b/boards/static/css/sw/base_page.css @@ -263,8 +263,13 @@ li { margin-left: 1ex; } +.moderator_info { + font-weight: bold; + float: right; +} + .refmap { border: 1px dashed #aaa; padding: 0.5em; display: table; -} \ No newline at end of file +} diff --git a/boards/templates/boards/base.html b/boards/templates/boards/base.html --- a/boards/templates/boards/base.html +++ b/boards/templates/boards/base.html @@ -24,19 +24,12 @@ -
- - {% if request.session.admin == True %} - Admin panel TODO: Need to implement
- {% endif %} - -
{% autoescape off %} {{ thread.text.rendered|truncatewords_html:50 }} @@ -103,8 +111,8 @@ {% endfor %} {% else %} - No threads found. -
+
+ {% trans 'No threads exist. Create the first one!' %}
{% endif %}
{% csrf_token %} @@ -163,6 +171,7 @@ {% endif %} ">{{ page }}] {% endfor %} + [RSS] {% endblock %} diff --git a/boards/templates/boards/settings.html b/boards/templates/boards/settings.html --- a/boards/templates/boards/settings.html +++ b/boards/templates/boards/settings.html @@ -8,6 +8,13 @@ {% block content %} +
+ {% trans 'User:' %} {{ user.user_id }}. + {% if user.is_moderator %} + {% trans 'You are moderator.' %} + {% endif %} +
+
{% trans "Theme" %} diff --git a/boards/templates/boards/tags.html b/boards/templates/boards/tags.html --- a/boards/templates/boards/tags.html +++ b/boards/templates/boards/tags.html @@ -4,20 +4,28 @@ {% load markup %} {% block head %} - Neboard - {% trans "tags" %} + Neboard - {% trans "Tags" %} {% endblock %} {% block content %}
- {% if tags %} + {% if all_tags %} {% for tag in all_tags %} - {{ tag.name }}
+ {{ tag.name }} + ({{ tag.get_post_count }} {% trans 'threads' %}) + {% if tag in user.fav_tags.all %} + [{% trans 'Remove' %}] + {% else %} + [{% trans 'Add' %}] + {% endif %} +
{% endfor %} {% else %} - No tags found. -
+ {% trans 'No tags found.' %} {% endif %}
diff --git a/boards/templates/boards/thread.html b/boards/templates/boards/thread.html --- a/boards/templates/boards/thread.html +++ b/boards/templates/boards/thread.html @@ -38,6 +38,14 @@ [{{ post.pub_time }}] [>>] + + {% if user.is_moderator %} + + ({{ post.poster_ip }}) + [{% trans 'Delete' %}] + + {% endif %}
{% autoescape off %} {{ post.text.rendered }} @@ -56,9 +64,6 @@
{% endfor %} - {% else %} - No thread found. -
{% endif %} RSS] {% endblock %} diff --git a/boards/tests.py b/boards/tests.py --- a/boards/tests.py +++ b/boards/tests.py @@ -4,7 +4,7 @@ from django.test.client import Client import boards -from boards.models import Post, Admin, Tag +from boards.models import Post, Tag from neboard import settings TEST_TEXT = 'test text' @@ -54,48 +54,6 @@ class BoardTests(TestCase): admin.save() return admin - def test_admin_login(self): - client = Client() - - self.assertFalse('admin' in client.session) - - admin = self._create_test_user() - - response = client.post('/login', - {'name': admin.name, 'password': admin.password}) - - # it means that login passed and user are redirected to another page - self.assertEqual(302, response.status_code) - - self.assertTrue('admin' in client.session) - self.assertTrue(client.session['admin']) - - admin.delete() - - wrong_name = 'sd2f1s3d21fs3d21f' - wrong_password = 'sd2f1s3d21fs3d21fsdfsd' - - client.post('/login', {'name': wrong_name, 'password': wrong_password}) - self.assertFalse(client.session['admin']) - - def test_admin_logout(self): - client = Client() - - self.assertFalse('admin' in client.session) - - admin = self._create_test_user() - - client.post('/login', - {'name': admin.name, 'password': admin.password}) - - self.assertTrue(client.session['admin']) - - client.get('/logout') - - self.assertFalse(client.session['admin']) - - admin.delete() - def test_get_thread(self): opening_post = self._create_post() op_id = opening_post.id @@ -120,13 +78,6 @@ class BoardTests(TestCase): self.assertEqual(settings.MAX_THREAD_COUNT, len(Post.objects.get_threads())) - def test_get(self): - """Test if the get computes properly""" - - post = self._create_post() - - self.assertTrue(post.is_get()) - def test_pages(self): """Test that the thread list is properly split into pages""" @@ -202,6 +153,7 @@ class BoardTests(TestCase): response_not_existing = client.get(THREAD_PAGE + str( existing_post_id + 1) + '/') + response_not_existing.get_full_path() self.assertEqual(HTTP_CODE_NOT_FOUND, response_not_existing.status_code, u'Not existing thread is opened') diff --git a/boards/thumbs.py b/boards/thumbs.py --- a/boards/thumbs.py +++ b/boards/thumbs.py @@ -9,6 +9,7 @@ from PIL import Image from django.core.files.base import ContentFile import cStringIO + def generate_thumb(img, thumb_size, format): """ Generates a thumbnail image and returns a ContentFile object with the thumbnail @@ -22,10 +23,10 @@ def generate_thumb(img, thumb_size, form format format of the original image ('jpeg','gif','png',...) (this format will be used for the generated thumbnail, too) """ - + img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details image = Image.open(img) - + # get size thumb_w, thumb_h = thumb_size # If you want to generate a square thumbnail @@ -33,12 +34,13 @@ def generate_thumb(img, thumb_size, form # quad xsize, ysize = image.size # get minimum size - minsize = min(xsize,ysize) + minsize = min(xsize, ysize) # largest square possible in the image - xnewsize = (xsize-minsize)/2 - ynewsize = (ysize-minsize)/2 + xnewsize = (xsize - minsize) / 2 + ynewsize = (ysize - minsize) / 2 # crop it - image2 = image.crop((xnewsize, ynewsize, xsize-xnewsize, ysize-ynewsize)) + image2 = image.crop( + (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize)) # load is necessary after crop image2.load() # thumbnail of the cropped image (with ANTIALIAS to make it look better) @@ -47,66 +49,70 @@ def generate_thumb(img, thumb_size, form # not quad image2 = image image2.thumbnail(thumb_size, Image.ANTIALIAS) - + io = cStringIO.StringIO() # PNG and GIF are the same, JPG is JPEG - if format.upper()=='JPG': + if format.upper() == 'JPG': format = 'JPEG' - + image2.save(io, format) - return ContentFile(io.getvalue()) + return ContentFile(io.getvalue()) + class ImageWithThumbsFieldFile(ImageFieldFile): """ See ImageWithThumbsField for usage example """ + def __init__(self, *args, **kwargs): super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs) self.sizes = self.field.sizes - + if self.sizes: def get_size(self, size): if not self: return '' else: - split = self.url.rsplit('.',1) - thumb_url = '%s.%sx%s.%s' % (split[0],w,h,split[1]) + split = self.url.rsplit('.', 1) + thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1]) return thumb_url - + for size in self.sizes: - (w,h) = size - setattr(self, 'url_%sx%s' % (w,h), get_size(self, size)) - + (w, h) = size + setattr(self, 'url_%sx%s' % (w, h), get_size(self, size)) + def save(self, name, content, save=True): super(ImageWithThumbsFieldFile, self).save(name, content, save) - + if self.sizes: for size in self.sizes: - (w,h) = size - split = self.name.rsplit('.',1) - thumb_name = '%s.%sx%s.%s' % (split[0],w,h,split[1]) - + (w, h) = size + split = self.name.rsplit('.', 1) + thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) + # you can use another thumbnailing function if you like thumb_content = generate_thumb(content, size, split[1]) - - thumb_name_ = self.storage.save(thumb_name, thumb_content) - + + thumb_name_ = self.storage.save(thumb_name, thumb_content) + if not thumb_name == thumb_name_: - raise ValueError('There is already a file named %s' % thumb_name) - + raise ValueError( + 'There is already a file named %s' % thumb_name) + def delete(self, save=True): - name=self.name + name = self.name super(ImageWithThumbsFieldFile, self).delete(save) if self.sizes: for size in self.sizes: - (w,h) = size - split = name.rsplit('.',1) - thumb_name = '%s.%sx%s.%s' % (split[0],w,h,split[1]) + (w, h) = size + split = name.rsplit('.', 1) + thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1]) try: self.storage.delete(thumb_name) except: pass - + + class ImageWithThumbsField(ImageField): attr_class = ImageWithThumbsFieldFile """ @@ -152,14 +158,16 @@ class ImageWithThumbsField(ImageField): """ - def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, sizes=None, **kwargs): - self.verbose_name=verbose_name - self.name=name - self.width_field=width_field - self.height_field=height_field + + def __init__(self, verbose_name=None, name=None, width_field=None, + height_field=None, sizes=None, **kwargs): + self.verbose_name = verbose_name + self.name = name + self.width_field = width_field + self.height_field = height_field self.sizes = sizes super(ImageField, self).__init__(**kwargs) from south.modelsinspector import add_introspection_rules -add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"]) \ No newline at end of file +add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"]) diff --git a/boards/urls.py b/boards/urls.py --- a/boards/urls.py +++ b/boards/urls.py @@ -14,22 +14,29 @@ urlpatterns = patterns('', url(r'^page/(?P\w+)/$', views.index, name='index'), # login page - url(r'^login$', views.login, name='login'), - # logout page - url(r'^logout$', views.logout, name='logout'), + url(r'^login/$', views.login, name='login'), # /boards/tag/tag_name/ url(r'^tag/(?P\w+)/$', views.tag, name='tag'), # /boards/tag/tag_id/page/ url(r'^tag/(?P\w+)/page/(?P\w+)/$', views.tag, name='tag'), + + # /boards/tag/tag_name/unsubscribe/ + url(r'^tag/(?P\w+)/subscribe/$', views.tag_subscribe, + name='tag_subscribe'), + # /boards/tag/tag_name/unsubscribe/ + url(r'^tag/(?P\w+)/unsubscribe/$', views.tag_unsubscribe, + name='tag_unsubscribe'), + # /boards/thread/ url(r'^thread/(?P\w+)/$', views.thread, name='thread'), # /boards/theme/theme_name/ - url(r'^settings$', views.settings, name='settings'), - url(r'^tags$', views.all_tags, name='tags'), + url(r'^settings/$', views.settings, name='settings'), + url(r'^tags/$', views.all_tags, name='tags'), url(r'^captcha/', include('captcha.urls')), url(r'^jump/(?P\w+)/$', views.jump_to_post, name='jumper'), url(r'^authors/$', views.authors, name='authors'), + url(r'^delete/(?P\w+)/$', views.delete, name='delete'), url(r'^banned/$', views.you_are_banned, name='banned'), # RSS feeds @@ -40,4 +47,4 @@ urlpatterns = patterns('', url(r'^thread/(?P\w+)/rss/$', ThreadPostsFeed()), url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), -) \ No newline at end of file +) diff --git a/boards/views.py b/boards/views.py --- a/boards/views.py +++ b/boards/views.py @@ -1,15 +1,17 @@ +import hashlib from django.core.urlresolvers import reverse from django.template import RequestContext from django.shortcuts import render, redirect, get_object_or_404 from django.http import HttpResponseRedirect +from django.utils import timezone from boards import forms import boards from boards import utils from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \ - ThreadCaptchaForm, PostCaptchaForm + ThreadCaptchaForm, PostCaptchaForm, LoginForm -from boards.models import Post, Admin, Tag, Ban +from boards.models import Post, Tag, Ban, User, RANK_USER, RANK_MODERATOR, NO_PARENT from boards import authors import neboard @@ -148,51 +150,43 @@ def thread(request, post_id): def login(request): - """Log in as admin""" - - if 'name' in request.POST and 'password' in request.POST: - request.session['admin'] = False + """Log in with user id""" - isAdmin = len(Admin.objects.filter(name=request.POST['name'], - password=request.POST[ - 'password'])) > 0 - - if isAdmin: - request.session['admin'] = True + context = _init_default_context(request) - response = HttpResponseRedirect('/') - - else: - response = render(request, 'boards/login.html', {'error': 'Login error'}) - else: - response = render(request, 'boards/login.html', {}) + if request.method == 'POST': + form = LoginForm(request.POST, request.FILES, error_class=PlainErrorList) + if form.is_valid(): + user = User.objects.get(user_id=form.cleaned_data['user_id']) + request.session['user_id'] = user.id + return redirect(index) - return response - + else: + form = LoginForm() -def logout(request): - request.session['admin'] = False - return HttpResponseRedirect('/') + context['form'] = form + + return render(request, 'boards/login.html', context) def settings(request): """User's settings""" - context = RequestContext(request) + context = _init_default_context(request) if request.method == 'POST': form = SettingsForm(request.POST) if form.is_valid(): selected_theme = form.cleaned_data['theme'] - request.session['theme'] = selected_theme + + user = _get_user(request) + user.save_setting('theme', selected_theme) return redirect(settings) else: selected_theme = _get_theme(request) form = SettingsForm(initial={'theme': selected_theme}) context['form'] = form - context['tags'] = Tag.objects.get_popular_tags() - context['theme'] = _get_theme(request) return render(request, 'boards/settings.html', context) @@ -226,6 +220,19 @@ def authors(request): return render(request, 'boards/authors.html', context) +def delete(request, post_id): + user = _get_user(request) + post = get_object_or_404(Post, id=post_id) + + if user.is_moderator(): + Post.objects.delete_post(post) + + if NO_PARENT == post.parent: + return redirect(index) + else: + return redirect(thread, post_id=post.parent) + + def you_are_banned(request): context = _init_default_context(request) return render(request, 'boards/banned.html', context) @@ -236,10 +243,35 @@ def page_404(request): return render(request, 'boards/404.html', context) +def tag_subscribe(request, tag_name): + user = _get_user(request) + tag = get_object_or_404(Tag, name=tag_name) + + if not tag in user.fav_tags.all(): + user.fav_tags.add(tag) + + return redirect(all_tags) + + +def tag_unsubscribe(request, tag_name): + user = _get_user(request) + tag = get_object_or_404(Tag, name=tag_name) + + if tag in user.fav_tags.all(): + user.fav_tags.remove(tag) + + return redirect(all_tags) + + def _get_theme(request): """Get user's CSS theme""" - return request.session.get('theme', neboard.settings.DEFAULT_THEME) + user = _get_user(request) + theme = user.get_setting('theme') + if not theme: + theme = neboard.settings.DEFAULT_THEME + + return theme def _get_client_ip(request): @@ -255,7 +287,34 @@ def _init_default_context(request): """Create context with default values that are used in most views""" context = RequestContext(request) - context['tags'] = Tag.objects.get_popular_tags() + context['user'] = _get_user(request) + context['tags'] = _get_user(request).fav_tags.all() context['theme'] = _get_theme(request) return context + + +def _get_user(request): + """Get current user from the session""" + + session = request.session + if not 'user_id' in session: + request.session.save() + + md5 = hashlib.md5() + md5.update(session.session_key) + new_id = md5.hexdigest() + + time_now = timezone.now() + user = User.objects.create(user_id=new_id, rank=RANK_USER, + registration_time=time_now, + last_access_time=time_now) + + session['user_id'] = user.id + else: + user = User.objects.get(id=session['user_id']) + user.save() + + user.last_access_time = timezone.now() + + return user diff --git a/neboard/settings.py b/neboard/settings.py --- a/neboard/settings.py +++ b/neboard/settings.py @@ -188,13 +188,14 @@ SITE_NAME = 'Neboard' THEMES = [ ('md', 'Mystic Dark'), - ('sw', 'Snow White') ] + ('sw', 'Snow White') +] DEFAULT_THEME = 'md' POPULAR_TAGS = 10 LAST_REPLIES_COUNT = 3 -ENABLE_CAPTCHA = True +ENABLE_CAPTCHA = False # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds