mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-30 13:23:14 +00:00
added func: set group avatar
This commit is contained in:
parent
cbab401d84
commit
75b724a169
@ -1,8 +1,30 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.template.defaultfilters import filesizeformat
|
||||||
|
|
||||||
|
from group.settings import ( AVATAR_MAX_SIZE, AVATAR_ALLOWED_FILE_EXTS)
|
||||||
|
|
||||||
class MessageForm(forms.Form):
|
class MessageForm(forms.Form):
|
||||||
message = forms.CharField(max_length=500)
|
message = forms.CharField(max_length=500)
|
||||||
|
|
||||||
class MessageReplyForm(forms.Form):
|
class MessageReplyForm(forms.Form):
|
||||||
message = forms.CharField(max_length=150)
|
message = forms.CharField(max_length=150)
|
||||||
|
|
||||||
|
class AvatarForm(forms.Form):
|
||||||
|
avatar = forms.ImageField()
|
||||||
|
|
||||||
|
def clean_avatar(self):
|
||||||
|
data = self.cleaned_data['avatar']
|
||||||
|
if AVATAR_ALLOWED_FILE_EXTS:
|
||||||
|
(root, ext) = os.path.splitext(data.name.lower())
|
||||||
|
if ext not in AVATAR_ALLOWED_FILE_EXTS:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_(u"%(ext)s is an invalid file extension. Authorized extensions are : %(valid_exts_list)s") %
|
||||||
|
{ 'ext' : ext, 'valid_exts_list' : ", ".join(AVATAR_ALLOWED_FILE_EXTS) })
|
||||||
|
if data.size > AVATAR_MAX_SIZE:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_(u"Your file is too big (%(size)s), the maximum allowed size is %(max_valid_size)s") %
|
||||||
|
{ 'size' : filesizeformat(data.size), 'max_valid_size' : filesizeformat(AVATAR_MAX_SIZE)})
|
||||||
|
|
||||||
|
104
group/models.py
104
group/models.py
@ -1,5 +1,26 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import os
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.utils.hashcompat import md5_constructor
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from group.settings import (AVATAR_STORAGE_DIR, AVATAR_RESIZE_METHOD,
|
||||||
|
AVATAR_MAX_AVATARS_PER_USER, AVATAR_THUMB_FORMAT,
|
||||||
|
AVATAR_HASH_USERDIRNAMES, AVATAR_HASH_FILENAMES,
|
||||||
|
AVATAR_THUMB_QUALITY, AUTO_GENERATE_AVATAR_SIZES)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cStringIO import StringIO
|
||||||
|
dir(StringIO) # Placate PyFlakes
|
||||||
|
except ImportError:
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
dir(Image) # Placate PyFlakes
|
||||||
|
except ImportError:
|
||||||
|
import Image
|
||||||
|
|
||||||
class GroupMessage(models.Model):
|
class GroupMessage(models.Model):
|
||||||
group_id = models.IntegerField()
|
group_id = models.IntegerField()
|
||||||
@ -12,3 +33,86 @@ class MessageReply(models.Model):
|
|||||||
from_email = models.EmailField()
|
from_email = models.EmailField()
|
||||||
message = models.CharField(max_length=150)
|
message = models.CharField(max_length=150)
|
||||||
timestamp = models.DateTimeField(default=datetime.datetime.now)
|
timestamp = models.DateTimeField(default=datetime.datetime.now)
|
||||||
|
|
||||||
|
def avatar_file_path(instance=None, filename=None, size=None, ext=None):
|
||||||
|
tmppath = [AVATAR_STORAGE_DIR]
|
||||||
|
if AVATAR_HASH_USERDIRNAMES:
|
||||||
|
tmp = md5_constructor(instance.user.username).hexdigest()
|
||||||
|
tmppath.extend([tmp[0], tmp[1], instance.group_id])
|
||||||
|
else:
|
||||||
|
tmppath.append(instance.group_id)
|
||||||
|
if not filename:
|
||||||
|
# Filename already stored in database
|
||||||
|
filename = instance.avatar.name
|
||||||
|
if ext and AVATAR_HASH_FILENAMES:
|
||||||
|
# An extension was provided, probably because the thumbnail
|
||||||
|
# is in a different format than the file. Use it. Because it's
|
||||||
|
# only enabled if AVATAR_HASH_FILENAMES is true, we can trust
|
||||||
|
# it won't conflict with another filename
|
||||||
|
(root, oldext) = os.path.splitext(filename)
|
||||||
|
filename = root + "." + ext
|
||||||
|
else:
|
||||||
|
# File doesn't exist yet
|
||||||
|
if AVATAR_HASH_FILENAMES:
|
||||||
|
(root, ext) = os.path.splitext(filename)
|
||||||
|
filename = md5_constructor(smart_str(filename)).hexdigest()
|
||||||
|
filename = filename + ext
|
||||||
|
if size:
|
||||||
|
tmppath.extend(['resized', str(size)])
|
||||||
|
tmppath.append(os.path.basename(filename))
|
||||||
|
return os.path.join(*tmppath)
|
||||||
|
|
||||||
|
def find_extension(format):
|
||||||
|
format = format.lower()
|
||||||
|
|
||||||
|
if format == 'jpeg':
|
||||||
|
format = 'jpg'
|
||||||
|
|
||||||
|
return format
|
||||||
|
|
||||||
|
class Avatar(models.Model):
|
||||||
|
group_id = models.CharField(max_length=255)
|
||||||
|
avatar = models.ImageField(max_length=1024, upload_to=avatar_file_path, blank=True)
|
||||||
|
date_uploaded = models.DateTimeField(default=datetime.datetime.now)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return _(u'Avatar for %s') % self.group_id
|
||||||
|
|
||||||
|
def thumbnail_exists(self, size):
|
||||||
|
return self.avatar.storage.exists(self.avatar_name(size))
|
||||||
|
|
||||||
|
def create_thumbnail(self, size, quality=None):
|
||||||
|
try:
|
||||||
|
orig = self.avatar.storage.open(self.avatar.name, 'rb').read()
|
||||||
|
image = Image.open(StringIO(orig))
|
||||||
|
except IOError:
|
||||||
|
return # What should we do here? Render a "sorry, didn't work" img?
|
||||||
|
quality = quality or AVATAR_THUMB_QUALITY
|
||||||
|
(w, h) = image.size
|
||||||
|
if w != size or h != size:
|
||||||
|
if w > h:
|
||||||
|
diff = (w - h) / 2
|
||||||
|
image = image.crop((diff, 0, w - diff, h))
|
||||||
|
else:
|
||||||
|
diff = (h - w) / 2
|
||||||
|
image = image.crop((0, diff, w, h - diff))
|
||||||
|
if image.mode != "RGB":
|
||||||
|
image = image.convert("RGB")
|
||||||
|
image = image.resize((size, size), AVATAR_RESIZE_METHOD)
|
||||||
|
thumb = StringIO()
|
||||||
|
image.save(thumb, AVATAR_THUMB_FORMAT, quality=quality)
|
||||||
|
thumb_file = ContentFile(thumb.getvalue())
|
||||||
|
else:
|
||||||
|
thumb_file = ContentFile(orig)
|
||||||
|
thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file)
|
||||||
|
|
||||||
|
def avatar_url(self, size):
|
||||||
|
return self.avatar.storage.url(self.avatar_name(size))
|
||||||
|
|
||||||
|
def avatar_name(self, size):
|
||||||
|
ext = find_extension(AVATAR_THUMB_FORMAT)
|
||||||
|
return avatar_file_path(
|
||||||
|
instance=self,
|
||||||
|
size=size,
|
||||||
|
ext=ext
|
||||||
|
)
|
||||||
|
23
group/settings.py
Normal file
23
group/settings.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
dir(Image) # Placate PyFlakes
|
||||||
|
except ImportError:
|
||||||
|
import Image
|
||||||
|
|
||||||
|
AVATAR_DEFAULT_SIZE = 48
|
||||||
|
AUTO_GENERATE_AVATAR_SIZES = (80, 48)
|
||||||
|
AVATAR_RESIZE_METHOD = getattr(settings, 'AVATAR_RESIZE_METHOD', Image.ANTIALIAS)
|
||||||
|
AVATAR_STORAGE_DIR = 'avatars/groups'
|
||||||
|
AVATAR_GRAVATAR_BACKUP = getattr(settings, 'AVATAR_GRAVATAR_BACKUP', True)
|
||||||
|
AVATAR_GRAVATAR_DEFAULT = getattr(settings, 'AVATAR_GRAVATAR_DEFAULT', None)
|
||||||
|
AVATAR_DEFAULT_URL = 'avatars/groups/default.png'
|
||||||
|
AVATAR_MAX_AVATARS_PER_USER = getattr(settings, 'AVATAR_MAX_AVATARS_PER_USER', 42)
|
||||||
|
AVATAR_MAX_SIZE = getattr(settings, 'AVATAR_MAX_SIZE', 1024 * 1024)
|
||||||
|
AVATAR_THUMB_FORMAT = getattr(settings, 'AVATAR_THUMB_FORMAT', "JPEG")
|
||||||
|
AVATAR_THUMB_QUALITY = getattr(settings, 'AVATAR_THUMB_QUALITY', 85)
|
||||||
|
AVATAR_HASH_FILENAMES = getattr(settings, 'AVATAR_HASH_FILENAMES', False)
|
||||||
|
AVATAR_HASH_USERDIRNAMES = getattr(settings, 'AVATAR_HASH_USERDIRNAMES', False)
|
||||||
|
AVATAR_ALLOWED_FILE_EXTS = getattr(settings, 'AVATAR_ALLOWED_FILE_EXTS', None)
|
||||||
|
AVATAR_CACHE_TIMEOUT = getattr(settings, 'AVATAR_CACHE_TIMEOUT', 60*60)
|
@ -10,8 +10,9 @@
|
|||||||
<div class="side fright">
|
<div class="side fright">
|
||||||
<h3>操作</h3>
|
<h3>操作</h3>
|
||||||
<ul class="with-bg">
|
<ul class="with-bg">
|
||||||
|
<li><a href="{{ SITE_ROOT }}group/{{ group.id }}/set_avatar/">设置小组图标</a></li>
|
||||||
<li><a id="group-remove" href="#" data="{{ SITE_ROOT }}group/{{ group.id }}/?op=delete">解散小组</a></li>
|
<li><a id="group-remove" href="#" data="{{ SITE_ROOT }}group/{{ group.id }}/?op=delete">解散小组</a></li>
|
||||||
<li><a id="group-info" href="{{ SITE_ROOT }}group/{{ group.id }}/">返回小组</a></li>
|
<li><a href="{{ SITE_ROOT }}group/{{ group.id }}/">返回小组</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% extends "myhome_base.html" %}
|
{% extends "myhome_base.html" %}
|
||||||
{% load seahub_tags %}
|
{% load seahub_tags group_tags %}
|
||||||
|
|
||||||
{% block nav_group_class %}class="cur"{% endblock %}
|
{% block nav_group_class %}class="cur"{% endblock %}
|
||||||
{% block left_panel %}
|
{% block left_panel %}
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<li class="group fleft">
|
<li class="group fleft">
|
||||||
<div class="pic fleft">
|
<div class="pic fleft">
|
||||||
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">
|
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">
|
||||||
<img src="{{ MEDIA_URL }}img/group.jpg" alt="小组图标" />
|
<img src="{% grp_avatar_url group.props.id 48 %}" alt="{{ group.props.group_name }}的图标" title="{{ group.props.group_name }}" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="txt fright">
|
<div class="txt fright">
|
||||||
@ -44,7 +44,7 @@
|
|||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$("#group-add").click(function() {
|
$("#group-add").click(function() {
|
||||||
$("#group-add-form").modal({appendTo: "#main", containerCss:{padding:18}});
|
$("#group-add-form").modal({appendTo: "#main"});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
21
group/templates/group/set_avatar.html
Normal file
21
group/templates/group/set_avatar.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends "myhome_base.html" %}
|
||||||
|
{% load group_tags %}
|
||||||
|
|
||||||
|
{% block nav_group_class %}class="cur"{% endblock %}
|
||||||
|
|
||||||
|
{% block main_panel %}
|
||||||
|
<div class="avatar-op">
|
||||||
|
<h2>修改小组 {{ group.group_name }} 的图标</h2>
|
||||||
|
<div class="avatar-op-con">
|
||||||
|
<h3>当前图标:</h3>
|
||||||
|
<img src="{% grp_avatar_url group.id %}" alt="当前图标" class="avatar" />
|
||||||
|
|
||||||
|
<h3 class="upload-new-avatar-hd">上传新图标:</h3>
|
||||||
|
<form enctype="multipart/form-data" method="post" action="">
|
||||||
|
{{ form.avatar }}<br />
|
||||||
|
{{ form.avatar.errors }}
|
||||||
|
<input type="submit" value="提交" class="submit" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
0
group/templatetags/__init__.py
Normal file
0
group/templatetags/__init__.py
Normal file
45
group/templatetags/group_tags.py
Normal file
45
group/templatetags/group_tags.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import urllib
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.utils.hashcompat import md5_constructor
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from group.settings import (AVATAR_DEFAULT_SIZE, AVATAR_DEFAULT_URL)
|
||||||
|
from group.models import Avatar
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
def get_default_avatar_url():
|
||||||
|
base_url = getattr(settings, 'STATIC_URL', None)
|
||||||
|
if not base_url:
|
||||||
|
base_url = getattr(settings, 'MEDIA_URL', '')
|
||||||
|
# Don't use base_url if the default avatar url starts with http:// of https://
|
||||||
|
if AVATAR_DEFAULT_URL.startswith('http://') or AVATAR_DEFAULT_URL.startswith('https://'):
|
||||||
|
return AVATAR_DEFAULT_URL
|
||||||
|
# We'll be nice and make sure there are no duplicated forward slashes
|
||||||
|
ends = base_url.endswith('/')
|
||||||
|
begins = AVATAR_DEFAULT_URL.startswith('/')
|
||||||
|
if ends and begins:
|
||||||
|
base_url = base_url[:-1]
|
||||||
|
elif not ends and not begins:
|
||||||
|
return '%s/%s' % (base_url, AVATAR_DEFAULT_URL)
|
||||||
|
return '%s%s' % (base_url, AVATAR_DEFAULT_URL)
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def grp_avatar_url(group_id, size=AVATAR_DEFAULT_SIZE):
|
||||||
|
grp_avatars = Avatar.objects.filter(group_id=group_id)
|
||||||
|
if grp_avatars:
|
||||||
|
avatar = grp_avatars.order_by('-date_uploaded')[0]
|
||||||
|
else:
|
||||||
|
avatar = None
|
||||||
|
|
||||||
|
if avatar:
|
||||||
|
if not avatar.thumbnail_exists(size):
|
||||||
|
avatar.create_thumbnail(size)
|
||||||
|
avatar_src = avatar.avatar_url(size)
|
||||||
|
else:
|
||||||
|
avatar_src = get_default_avatar_url()
|
||||||
|
|
||||||
|
return avatar_src
|
@ -1,12 +1,13 @@
|
|||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
from views import group_list, group_info, group_member_operations, \
|
from views import group_list, group_info, group_member_operations, \
|
||||||
group_members, msg_reply, msg_reply_new
|
group_members, msg_reply, msg_reply_new, set_avatar
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^(?P<group_id>[\d]+)/$', group_info, name='group_info'),
|
url(r'^(?P<group_id>[\d]+)/$', group_info, name='group_info'),
|
||||||
url(r'^reply/(?P<msg_id>[\d]+)/$', msg_reply, name='msg_reply'),
|
url(r'^reply/(?P<msg_id>[\d]+)/$', msg_reply, name='msg_reply'),
|
||||||
url(r'^reply/new/$', msg_reply_new, name='msg_reply_new'),
|
url(r'^reply/new/$', msg_reply_new, name='msg_reply_new'),
|
||||||
|
url(r'^(?P<group_id>[\d]+)/set_avatar/$', set_avatar, name='set_avatar'),
|
||||||
url(r'^(?P<group_id>[\d]+)/members/$', group_members, name='group_members'),
|
url(r'^(?P<group_id>[\d]+)/members/$', group_members, name='group_members'),
|
||||||
(r'^(?P<group_id>[\d]+)/member/(?P<user_name>[^/]+)/$', group_member_operations),
|
(r'^(?P<group_id>[\d]+)/member/(?P<user_name>[^/]+)/$', group_member_operations),
|
||||||
)
|
)
|
||||||
|
@ -10,8 +10,8 @@ from seaserv import ccnet_rpc, ccnet_threaded_rpc, seafserv_threaded_rpc, get_re
|
|||||||
get_group_repoids, check_group_staff, get_commits
|
get_group_repoids, check_group_staff, get_commits
|
||||||
from pysearpc import SearpcError
|
from pysearpc import SearpcError
|
||||||
|
|
||||||
from models import GroupMessage, MessageReply
|
from models import GroupMessage, MessageReply, Avatar
|
||||||
from forms import MessageForm, MessageReplyForm
|
from forms import MessageForm, MessageReplyForm, AvatarForm
|
||||||
from signals import grpmsg_added, grpmsg_reply_added
|
from signals import grpmsg_added, grpmsg_reply_added
|
||||||
from seahub.contacts.models import Contact
|
from seahub.contacts.models import Contact
|
||||||
from seahub.notifications.models import UserNotification
|
from seahub.notifications.models import UserNotification
|
||||||
@ -457,3 +457,32 @@ def group_unshare_repo(request, repo_id, group_id, from_email):
|
|||||||
|
|
||||||
if seafserv_threaded_rpc.group_unshare_repo(repo_id, group_id, from_email) != 0:
|
if seafserv_threaded_rpc.group_unshare_repo(repo_id, group_id, from_email) != 0:
|
||||||
return go_error(request, u'共享失败:内部错误')
|
return go_error(request, u'共享失败:内部错误')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def set_avatar(request, group_id):
|
||||||
|
try:
|
||||||
|
group_id_int = int(group_id)
|
||||||
|
except ValueError:
|
||||||
|
return go_error(request, u'group id 不是有效参数')
|
||||||
|
|
||||||
|
if not check_group_staff(group_id_int, request.user):
|
||||||
|
return go_permission_error(request, u'只有小组管理员有权设置小组图标')
|
||||||
|
|
||||||
|
group = ccnet_threaded_rpc.get_group(group_id_int)
|
||||||
|
if not group:
|
||||||
|
return HttpResponseRedirect(reverse('group_list', args=[]))
|
||||||
|
|
||||||
|
form = AvatarForm(request.POST or None, request.FILES or None)
|
||||||
|
|
||||||
|
if request.method == 'POST' and 'avatar' in request.FILES:
|
||||||
|
if form.is_valid():
|
||||||
|
image_file = request.FILES['avatar']
|
||||||
|
avatar = Avatar()
|
||||||
|
avatar.avatar.save(image_file.name, image_file)
|
||||||
|
avatar.group_id = group_id
|
||||||
|
avatar.save()
|
||||||
|
|
||||||
|
return render_to_response('group/set_avatar.html', {
|
||||||
|
'group' : group,
|
||||||
|
'form' : form,
|
||||||
|
}, context_instance=RequestContext(request))
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 751 B |
BIN
media/avatars/groups/default.png
Normal file
BIN
media/avatars/groups/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.7 KiB |
@ -1,5 +1,5 @@
|
|||||||
{% extends "myhome_base.html" %}
|
{% extends "myhome_base.html" %}
|
||||||
{% load seahub_tags avatar_tags %}
|
{% load seahub_tags avatar_tags group_tags %}
|
||||||
|
|
||||||
{% block nav_myhome_class %}class="cur"{% endblock %}
|
{% block nav_myhome_class %}class="cur"{% endblock %}
|
||||||
{% block left_panel %}
|
{% block left_panel %}
|
||||||
@ -31,7 +31,9 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for group in groups_manage %}
|
{% for group in groups_manage %}
|
||||||
<li class="mygroup">
|
<li class="mygroup">
|
||||||
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/"><img src="{{ MEDIA_URL }}img/group.jpg" alt="" /></a><br />
|
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">
|
||||||
|
<img src="{% grp_avatar_url group.props.id 48 %}" alt="{{ group.props.group_name }}的图标" title="{{ group.props.group_name }}" />
|
||||||
|
</a><br />
|
||||||
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">{{ group.props.group_name }}</a>
|
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">{{ group.props.group_name }}</a>
|
||||||
{% if group.new_msg %}
|
{% if group.new_msg %}
|
||||||
<span class="tips">(新留言)</span>
|
<span class="tips">(新留言)</span>
|
||||||
@ -48,7 +50,9 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for group in groups_join %}
|
{% for group in groups_join %}
|
||||||
<li class="mygroup">
|
<li class="mygroup">
|
||||||
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/"><img src="{{ MEDIA_URL }}img/group.jpg" alt="" /></a><br />
|
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">
|
||||||
|
<img src="{% grp_avatar_url group.props.id 48 %}" alt="{{ group.props.group_name }}的图标" title="{{ group.props.group_name }}" />
|
||||||
|
</a><br />
|
||||||
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">{{ group.props.group_name }}</a>
|
<a href="{{ SITE_ROOT }}group/{{ group.props.id }}/">{{ group.props.group_name }}</a>
|
||||||
{% if group.new_msg %}
|
{% if group.new_msg %}
|
||||||
<span class="tips">(新留言)</span>
|
<span class="tips">(新留言)</span>
|
||||||
|
Loading…
Reference in New Issue
Block a user