Skip to content

Commit

Permalink
Merge pull request #340 from loleg/main
Browse files Browse the repository at this point in the history
Project stats and team view
  • Loading branch information
loleg committed Mar 27, 2023
2 parents 041267d + 6d35984 commit 0037109
Show file tree
Hide file tree
Showing 40 changed files with 485 additions and 278 deletions.
32 changes: 32 additions & 0 deletions cli.py
Expand Up @@ -33,6 +33,37 @@ def socialize(kind):
print("Updated %d users." % len(q))


@click.command()
@click.argument('event', required=True)
@click.argument('clear', required=False, default=False)
@click.argument('primes', required=False, default=True)
@click.argument('challenges', required=False, default=True)
def numerise(event: int, clear: bool, primes: bool, challenges: bool):
"""Assign numbers to challenge hashtags for an EVENT ID."""
# TODO: use a parameter for sort order alphabetic, id-based, etc.
if primes:
# Generate some primes, thanks @DhanushNayak
nq = list(filter(lambda x: not list(filter(lambda y : x%y==0, range(2,x))),range(2,200)))
else:
nq = list(range(1,200))
with create_app().app_context():
from dribdat.user.models import Event
projects = Event.query.filter_by(id=event) \
.first_or_404().projects
ix = 0
for c in projects:
if c.is_hidden: continue
if challenges and not c.is_challenge: continue
if not challenges and c.is_challenge: continue
ch = "" # push existing hashtag aside
if not clear and len(c.hashtag) > 0:
ch = " " + ch
c.hashtag = str(nq[ix]) + ch
c.save()
ix = ix + 1
print("Enumerated %d projects." % len(projects))


@click.command()
@click.argument('name', required=True)
@click.argument('start', required=False)
Expand Down Expand Up @@ -104,6 +135,7 @@ def cli():


cli.add_command(socialize)
cli.add_command(numerise)
cli.add_command(imports)
cli.add_command(exports)

Expand Down
4 changes: 2 additions & 2 deletions dribdat/admin/forms.py
Expand Up @@ -176,7 +176,7 @@ class ProjectForm(FlaskForm):
source_url = URLField(u'Source link', [length(max=2048)])
download_url = URLField(u'Download link', [length(max=2048)])
image_url = URLField(u'Image link', [length(max=255)])
logo_color = StringField(u'Custom color', [length(max=7)])
logo_color = StringField(u'Custom color')
logo_icon = StringField(u'Custom icon', [length(max=20)])
submit = SubmitField(u'Save')

Expand All @@ -188,7 +188,7 @@ class CategoryForm(FlaskForm):
name = StringField(u'Name', [length(max=80), DataRequired()])
description = TextAreaField(u'Description',
description=u'Markdown and HTML supported')
logo_color = StringField(u'Custom color', [length(max=7)])
logo_color = StringField(u'Custom color')
logo_icon = StringField(u'Custom icon', [length(max=20)],
description=u'fontawesome.com/v4/cheatsheet')
event_id = SelectField(u'Specific to an event, or global if blank',
Expand Down
18 changes: 11 additions & 7 deletions dribdat/admin/views.py
Expand Up @@ -76,11 +76,11 @@ def users(page=1):
)
elif sort_by == 'sso':
users = User.query.order_by(
User.sso_id.asc()
User.sso_id.desc()
)
elif sort_by == 'created':
elif sort_by == 'updated':
users = User.query.order_by(
User.created_at.desc()
User.updated_at.desc()
)
elif sort_by == 'email':
users = User.query.order_by(
Expand All @@ -90,16 +90,20 @@ def users(page=1):
users = User.query.order_by(
User.username.asc()
)
else: # Default: updated
else: # Default: created
sort_by = 'created'
users = User.query.order_by(
User.updated_at.desc()
User.created_at.desc()
)
search_by = request.args.get('search')
if search_by and len(search_by) > 1:
q = "%%%s%%" % search_by.lower()
users = users.filter(User.username.ilike(q))
if '@' in search_by:
users = users.filter(User.email.ilike(q))
else:
users = users.filter(User.username.ilike(q))
users = users.paginate(page, per_page=20)
return render_template('admin/users.html',
return render_template('admin/users.html', sort_by=sort_by,
data=users, endpoint='admin.users', active='users')


Expand Down
17 changes: 12 additions & 5 deletions dribdat/aggregation.py
Expand Up @@ -14,6 +14,7 @@
)
import json
import re
from sqlalchemy import and_


def GetProjectData(url):
Expand Down Expand Up @@ -58,6 +59,8 @@ def get_github_project(url):
apiurl = apiurl[:-4]
if apiurl == url:
return {}
if apiurl.endswith('.md'):
return FetchWebProject(url)
return FetchGithubProject(apiurl)


Expand Down Expand Up @@ -181,11 +184,15 @@ def GetEventUsers(event):
return None
users = []
userlist = []
for p in event.projects:
for u in p.get_team():
if u.id not in userlist:
userlist.append(u.id)
users.append(u)
projects = set([p.id for p in event.projects])
activities = Activity.query.filter(and_(
Activity.name=='star',
Activity.project_id.in_(projects)
)).all()
for a in activities:
if a.user and a.user_id not in userlist:
userlist.append(a.user_id)
users.append(a.user)
return sorted(users, key=lambda x: x.username)


Expand Down
26 changes: 23 additions & 3 deletions dribdat/apifetch.py
Expand Up @@ -297,6 +297,12 @@ def FetchWebProject(project_url):
# Google Document
if project_url.startswith('https://docs.google.com/document'):
return FetchWebGoogleDoc(data.text, project_url)
# Instructables
elif project_url.startswith('https://www.instructables.com/'):
return FetchWebInstructables(data.text, project_url)
# GitHub Markdown
elif project_url.startswith('https://github.com/'):
return FetchWebGitHub(project_url)
# CodiMD / HackMD
elif data.text.find('<div id="doc" ') > 0:
return FetchWebCodiMD(data.text, project_url)
Expand All @@ -306,9 +312,6 @@ def FetchWebProject(project_url):
# Etherpad
elif data.text.find('pad.importExport.exportetherpad') > 0:
return FetchWebEtherpad(data.text, project_url)
# Instructables
elif project_url.startswith('https://www.instructables.com/'):
return FetchWebInstructables(data.text, project_url)


def FetchWebGoogleDoc(text, url):
Expand Down Expand Up @@ -417,6 +420,23 @@ def FetchWebInstructables(text, url):
return obj


def FetchWebGitHub(url):
"""Grab a Markdown source from a GitHub link."""
if not url.endswith('.md') or not '/blob/' in url:
return {}
filename = url.split('/')[-1].replace('.md', '')
rawurl = url.replace('/blob/', '/raw/').replace("https://github.com/", '')
rawdata = requests.get("https://github.com/" + rawurl, timeout=REQUEST_TIMEOUT)
text_content = rawdata.text or ""
return {
'type': 'Markdown',
'name': filename,
'description': text_content,
'source_url': url,
'logo_icon': 'outdent',
}


def ParseInstructablesPage(content):
"""Create an HTML summary of content."""
html_content = ""
Expand Down
4 changes: 2 additions & 2 deletions dribdat/apipackage.py
Expand Up @@ -26,7 +26,7 @@ def event_to_data_package(event, author=None, host_url='', full_content=False):
"""Create a Data Package from the data of an event."""
# Define the author, if available
contributors = []
if author and not author.is_anonymous:
if author and not author.is_anonymous and author.is_admin:
contributors.append({
"title": author.username,
"path": author.webpage_url or '',
Expand Down Expand Up @@ -77,7 +77,7 @@ def event_to_data_package(event, author=None, host_url='', full_content=False):
name='activities',
data=get_event_activities(event.id, 500),
))
# print("Generating in-memory JSON of activities")
# print("Generating in-memory JSON of categories")
package.add_resource(Resource(
name='categories',
data=get_event_categories(event.id),
Expand Down
2 changes: 2 additions & 0 deletions dribdat/app.py
Expand Up @@ -173,10 +173,12 @@ def until_date(value):

@app.template_filter()
def format_date(value, format='%d.%m.%Y'):
if value is None: return ''
return value.strftime(format)

@app.template_filter()
def format_datetime(value, format='%d.%m.%Y %H:%M'):
if value is None: return ''
return value.strftime(format)


Expand Down
11 changes: 4 additions & 7 deletions dribdat/mailer.py
Expand Up @@ -5,12 +5,7 @@
from dribdat.utils import random_password # noqa: I005


async def send_async_email(app, msg):
with app.app_context():
msg.send()


def user_activation(app, user):
async def user_activation(app, user):
"""Send an activation by e-mail."""
act_hash = random_password(32)
with app.app_context():
Expand All @@ -29,4 +24,6 @@ def user_activation(app, user):
+ "Tap here to activate your account:\n\n%s" % act_url
msg.to = [user.email]
app.logger.info('Sending mail to user %d' % user.id)
send_async_email(app, msg)
await msg.send()
return True

5 changes: 3 additions & 2 deletions dribdat/public/api.py
Expand Up @@ -13,7 +13,7 @@
from flask_login import login_required, current_user
from sqlalchemy import or_

from ..extensions import db
from ..extensions import db, cache
from ..utils import timesince, random_password
from ..decorators import admin_required
from ..user.models import Event, Project, Activity
Expand Down Expand Up @@ -495,8 +495,8 @@ def generate_event_package(event, format='json'):
package.to_zip(fp_package.name)
return send_file(fp_package.name, as_attachment=True)


@blueprint.route('/event/current/datapackage.<format>', methods=["GET"])
@cache.cached()
def package_current_event(format):
"""Download a Data Package for an event."""
event = Event.query.filter_by(is_current=True).first() or \
Expand All @@ -505,6 +505,7 @@ def package_current_event(format):


@blueprint.route('/event/<int:event_id>/datapackage.<format>', methods=["GET"])
@cache.cached()
def package_specific_event(event_id, format):
"""Download a Data Package for an event."""
event = Event.query.filter_by(id=event_id).first_or_404()
Expand Down
26 changes: 17 additions & 9 deletions dribdat/public/auth.py
Expand Up @@ -53,6 +53,12 @@ def login():
# Handle logging in
if request.method == 'POST':
if form.validate_on_submit():
# Allow login with e-mail address
if '@' in form.username.data:
user_by_email = User.query.filter_by(email=form.username.data).first()
if user_by_email:
form.username.data = user_by_email.username
# Validate user account
login_user(form.user, remember=True)
if not form.user.active:
# Note: continue to profile page, where user is warned
Expand All @@ -70,7 +76,7 @@ def login():


@blueprint.route("/register/", methods=['GET', 'POST'])
def register():
async def register():
"""Register new user."""
if current_app.config['DRIBDAT_NOT_REGISTER']:
flash("Registration currently not possible.", 'warning')
Expand All @@ -87,8 +93,9 @@ def register():
logout_user()
return render_template('public/register.html',
form=form, oauth_type=oauth_type())
# Continue with user creation
# Double check username
sane_username = sanitize_input(form.username.data)
# Continue with user creation
new_user = User.create(
username=sane_username,
email=form.email.data,
Expand All @@ -106,7 +113,7 @@ def register():
new_user.active = False
new_user.save()
if current_app.config['MAIL_SERVER']:
user_activation(current_app, new_user)
await user_activation(current_app, new_user)
flash("New accounts require activation. "
+ "Please click the dribdat link in your e-mail.", 'success')
else:
Expand Down Expand Up @@ -160,7 +167,7 @@ def forgot():


@blueprint.route("/passwordless/", methods=['POST'])
def passwordless():
async def passwordless():
"""Log in a new user via e-mail."""
if current_app.config['DRIBDAT_NOT_REGISTER'] or \
not current_app.config['MAIL_SERVER']:
Expand All @@ -179,7 +186,7 @@ def passwordless():
a_user = User.query.filter_by(email=form.email.data).first()
if a_user:
# Continue with reset
user_activation(current_app, a_user)
await user_activation(current_app, a_user)
else:
current_app.logger.warn('User not found: %s' % form.email.data)
# Don't let people spy on your address
Expand Down Expand Up @@ -423,12 +430,13 @@ def mattermost_login():
return redirect(url_for("auth.login", local=1))
resp_data = resp.json()
# print(resp_data)
# Parse user data
username = None
if 'nickname' in resp_data:
if 'nickname' in resp_data and resp_data['nickname']:
username = resp_data['nickname']
elif 'username' in resp_data:
elif 'username' in resp_data and resp_data['username']:
username = resp_data['username']
if username is None:
if username is None or not 'email' in resp_data or not 'id' in resp_data:
flash('Invalid Mattermost data format', 'danger')
return redirect(url_for("auth.login", local=1))
return get_or_create_sso_user(
Expand All @@ -450,7 +458,7 @@ def hitobito_login():
flash('Unable to access hitobito data', 'danger')
return redirect(url_for("auth.login", local=1))
resp_data = resp.json()
#print(resp_data)
# print(resp_data)
username = None
if 'nickname' in resp_data and resp_data['nickname'] is not None:
username = resp_data['nickname']
Expand Down
2 changes: 1 addition & 1 deletion dribdat/public/forms.py
Expand Up @@ -99,7 +99,7 @@ class ProjectDetailForm(FlaskForm):
u'Cover image', [length(max=255)],
description="URL of an image to display at the top of the page.")
logo_color = StringField(
u'Color scheme', [length(max=7)],
u'Color scheme',
description="Customize the color of your project page.")
logo_icon = StringField(
u'Named icon',
Expand Down
6 changes: 3 additions & 3 deletions dribdat/public/views.py
Expand Up @@ -154,7 +154,7 @@ def user(username):
@login_required
def user_post():
"""Redirect to a Post form for my current project."""
projects = current_user.joined_projects(False)
projects = current_user.joined_projects(True, 1)
if not len(projects) > 0:
flash('Please Join a project to be able to Post an update.', 'info')
return redirect(url_for("public.home"))
Expand Down Expand Up @@ -256,8 +256,8 @@ def event_print(event_id):
now = datetime.utcnow().strftime("%d.%m.%Y %H:%M")
event = Event.query.filter_by(id=event_id).first_or_404()
eventdata = Project.query.filter_by(event_id=event_id, is_hidden=False)
projects = eventdata.filter(Project.progress >= 0).order_by(Project.name)
challenges = eventdata.filter(Project.progress < 0).order_by(Project.name)
projects = eventdata.filter(Project.progress > 0).order_by(Project.name)
challenges = eventdata.filter(Project.progress <= 0).order_by(Project.id)
return render_template('public/eventprint.html', active='print',
projects=projects, challenges=challenges,
current_event=event, curdate=now)
Expand Down
1 change: 1 addition & 0 deletions dribdat/settings.py
Expand Up @@ -39,6 +39,7 @@ class Config(object):
OAUTH_AUTH_URL = os_env.get('OAUTH_AUTH_URL', None)
# (Optional) Go directly to external login screen
OAUTH_SKIP_LOGIN = bool(strtobool(os_env.get('OAUTH_SKIP_LOGIN', 'False')))
OAUTH_LINK_REGISTER = os_env.get('OAUTH_LINK_REGISTER', None)

# Mail service support
MAIL_PORT = os_env.get('MAIL_PORT', 25)
Expand Down

0 comments on commit 0037109

Please sign in to comment.