mirror of
https://github.com/peeringdb/peeringdb.git
synced 2024-05-11 05:55:09 +00:00
Qu1003 (#621)
* use new peeringdb client (1.0.0) for pdb_load_data sync (#599) * drop django-mobi for lack of py3/dj2 support (#492) remove django-forms-bootstrap for lack of py3/dj2 support (#492) * black formatted * django2.2 and py3 upgrade (#492) * drop ixlans (#21) ui and api changes * drop local_asn (#168) * org search (#193) * phone number validation (#50) * implement help text tooltips (#228) * Mark own ASN as transit-free (#394) * py3 fix for `pdb_migrate_ixlans` command when writing migration report * pdb_migrate_ixlans: properly handle py3 Runtime error if ixlan dict changes during iteration * set rest DEFAULT_SCHEMA_CLASS to coreapi to fix swagger apidocs fix migration 0027 missing from facsimile manifest * fix swagger doc strings * fix tests that were broken from api doc fixes * fix UniqueFieldValidator for netixlan ipaddress validation that broke during django/drf upgrade * fix org merge tool layout issues * travis config * update pipfile and lock * black formatting * update travis dist * beta mode banner (#411) * add beta banner template (#411) * automatically scheduled sync may not always be on, add a flag that lets us reflect that state in the beta banner message clean up beta banner implementation (#411) * add tests for beta banner (#411)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-03 22:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
@@ -61,9 +61,9 @@ class Migration(migrations.Migration):
|
||||
unique=True,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
b"^[\\w\\.@+-=|/]+$",
|
||||
r"^[\\w\\.@+-=|/]+$",
|
||||
"Enter a valid username.",
|
||||
b"invalid",
|
||||
"invalid",
|
||||
flags=32,
|
||||
)
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-21 05:08
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-23 11:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-24 15:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-30 15:27
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-18 05:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-19 11:37
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-20 08:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-20 11:08
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-10-25 14:54
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-11-08 10:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-02-25 16:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-03-26 19:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-06-05 06:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.15 on 2019-01-10 23:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.15 on 2019-03-20 08:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.15 on 2019-03-20 08:09
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-08-19 11:33
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.manager
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-11-07 08:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-11-04 08:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-11-07 10:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2020-01-02 09:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("peeringdb_server", "0021_sponsorship_drop_single_org_relation_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="ixlan",
|
||||
name="id",
|
||||
field=models.IntegerField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
116
peeringdb_server/migrations/0023_netfac_local_asn.py
Normal file
116
peeringdb_server/migrations/0023_netfac_local_asn.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-12-10 12:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.utils import IntegrityError
|
||||
from django.db.models import Count
|
||||
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
|
||||
"""
|
||||
since `local_asn` will no longer be included as part of the
|
||||
unique key for netfac we need to remove any potential dupes
|
||||
that may exist because of that
|
||||
|
||||
We do this by setting the related network to be the network that
|
||||
matches the local_asn value
|
||||
"""
|
||||
|
||||
NetworkFacility = apps.get_model("peeringdb_server", "NetworkFacility")
|
||||
Network = apps.get_model("peeringdb_server", "Network")
|
||||
|
||||
removed_dupe = 0
|
||||
asn_missing = 0
|
||||
migrated = 0
|
||||
|
||||
print("\nmigrating netfacs")
|
||||
|
||||
for netfac in NetworkFacility.handleref.all():
|
||||
if netfac.local_asn != netfac.network.asn:
|
||||
|
||||
# local_asn differs from related network asn
|
||||
|
||||
# next check if the network facility we are about
|
||||
# to migrate already exists, if it does, skip it
|
||||
|
||||
qset = NetworkFacility.handleref.filter(
|
||||
network__asn=netfac.local_asn,
|
||||
facility_id=netfac.facility_id,
|
||||
local_asn=netfac.local_asn,
|
||||
)
|
||||
|
||||
if qset.exists():
|
||||
notes = "Removed duplicate netfac for AS{} @ fac{}".format(
|
||||
netfac.local_asn, netfac.facility_id
|
||||
)
|
||||
netfac.notes = notes
|
||||
netfac.status = "deleted"
|
||||
netfac.local_asn = None
|
||||
netfac.save()
|
||||
removed_dupe += 1
|
||||
continue
|
||||
|
||||
net = Network.handleref.filter(asn=netfac.local_asn).first()
|
||||
|
||||
if net and netfac.status == "ok":
|
||||
|
||||
# set network object from local_asn
|
||||
|
||||
if net.status != netfac.status:
|
||||
|
||||
# network status doesnt match netfac status
|
||||
# we update netfac status to match as that is the least
|
||||
# destructive behaviour while allowing us to still
|
||||
# resolve the unique constraint conflict
|
||||
|
||||
netfac.status = net.status
|
||||
|
||||
print(
|
||||
"AS{}: netfac{} at facility {} has been moved to network AS{}, however status"
|
||||
" between the two was mismatching and has been corrected, "
|
||||
"but should be reviewed".format(
|
||||
netfac.network.asn, netfac.facility.id, netfac.id, net.asn
|
||||
)
|
||||
)
|
||||
|
||||
netfac.network = net
|
||||
netfac.save()
|
||||
migrated += 1
|
||||
|
||||
else:
|
||||
# could not find network with asn matching local_asn
|
||||
# in this case we should drop the netfac and log it
|
||||
|
||||
notes = "AS{}: Could not correct non-existant local_asn AS{} @ fac{} ".format(
|
||||
netfac.network.asn, netfac.local_asn, netfac.facility.id
|
||||
)
|
||||
if netfac.status == "ok":
|
||||
print(notes)
|
||||
asn_missing += 1
|
||||
|
||||
continue
|
||||
|
||||
print("Changed related network for {} netfacs".format(migrated))
|
||||
print(
|
||||
"Deleted {} netfacs for being dupes after removal of local_asn".format(
|
||||
removed_dupe
|
||||
)
|
||||
)
|
||||
print(
|
||||
"Found {} netfacs where network matching local_asn did not exist".format(
|
||||
asn_missing
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("peeringdb_server", "0022_ixlan_remove_auto_increment"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||
]
|
||||
83
peeringdb_server/migrations/0024_netixlan_asn.py
Normal file
83
peeringdb_server/migrations/0024_netixlan_asn.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-12-10 15:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
|
||||
NetworkIXLan = apps.get_model("peeringdb_server", "NetworkIXLan")
|
||||
Network = apps.get_model("peeringdb_server", "Network")
|
||||
|
||||
asn_missing = 0
|
||||
migrated = 0
|
||||
|
||||
print("\nmigrating netixlans")
|
||||
|
||||
for netixlan in NetworkIXLan.handleref.all():
|
||||
if netixlan.asn != netixlan.network.asn:
|
||||
|
||||
# asn differs from related network asn
|
||||
|
||||
net = Network.handleref.filter(asn=netixlan.asn).first()
|
||||
|
||||
if net and netixlan.status == "ok":
|
||||
|
||||
# set network object from asn
|
||||
|
||||
if net.status != netixlan.status:
|
||||
|
||||
# network status doesnt match netixlan status
|
||||
# we update netixlan status to match as that is the least
|
||||
# destructive behaviour while allowing us to still
|
||||
# resolve the unique constraint conflict
|
||||
|
||||
netixlan.status = net.status
|
||||
|
||||
print(
|
||||
"AS{}: netixlan{} {} {} at exchange {} has been moved to network AS{}, however status"
|
||||
" between the two was mismatching "
|
||||
" and has been corrected, but should be reviewed".format(
|
||||
netixlan.network.asn,
|
||||
netixlan.id,
|
||||
netixlan.ipaddr4,
|
||||
netixlan.ipaddr6,
|
||||
netixlan.ixlan.ix.id,
|
||||
net.asn,
|
||||
)
|
||||
)
|
||||
|
||||
netixlan.network = net
|
||||
netixlan.save()
|
||||
migrated += 1
|
||||
|
||||
else:
|
||||
|
||||
# could not find network with asn matching asn
|
||||
# in this case we should drop the netixlan and log it
|
||||
|
||||
notes = "AS{}: Could not correct non-existant local_asn AS{} @ ixlan{} ".format(
|
||||
netixlan.network.asn, netixlan.asn, netixlan.ixlan.id
|
||||
)
|
||||
if netixlan.status == "ok":
|
||||
print(notes)
|
||||
asn_missing += 1
|
||||
|
||||
print("Changed related network for {} netixlans".format(migrated))
|
||||
print(
|
||||
"Found {} netixlans where network matching local_asn did not exist".format(
|
||||
asn_missing
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("peeringdb_server", "0023_netfac_local_asn"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||
]
|
||||
142
peeringdb_server/migrations/0025_E164_phonenumbers.py
Normal file
142
peeringdb_server/migrations/0025_E164_phonenumbers.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-12-12 08:46
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import csv
|
||||
import phonenumbers
|
||||
|
||||
from django.db import migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def _edit_url(tag, instance):
|
||||
if tag == "poc":
|
||||
return "{}/net/{}/".format(settings.BASE_URL, instance.network_id)
|
||||
else:
|
||||
return "{}/ix/{}/".format(settings.BASE_URL, instance.id)
|
||||
|
||||
|
||||
def _fix_number(tag, instance, field_name, list_fixed, list_invalid):
|
||||
number = getattr(instance, field_name, None).strip()
|
||||
if number:
|
||||
try:
|
||||
country = getattr(instance, "country", None)
|
||||
if country:
|
||||
country = country.code
|
||||
parsed_number = phonenumbers.parse(number, country)
|
||||
validated_number = phonenumbers.format_number(
|
||||
parsed_number, phonenumbers.PhoneNumberFormat.E164
|
||||
)
|
||||
|
||||
if "{}".format(validated_number) == "{}".format(number):
|
||||
return
|
||||
|
||||
setattr(instance, field_name, validated_number)
|
||||
list_fixed.append(
|
||||
[
|
||||
tag,
|
||||
instance.id,
|
||||
_edit_url(tag, instance),
|
||||
instance.status,
|
||||
field_name,
|
||||
number,
|
||||
validated_number,
|
||||
country,
|
||||
]
|
||||
)
|
||||
print("FIXED", tag, instance.id, field_name, number, validated_number)
|
||||
instance.save()
|
||||
except Exception as exc:
|
||||
_push_invalid(
|
||||
tag, instance, field_name, number, list_invalid, "{}".format(exc)
|
||||
)
|
||||
print("INVALID", tag, instance.id, field_name, number)
|
||||
|
||||
|
||||
def _push_invalid(tag, instance, field_name, number, list_invalid, reason):
|
||||
country = getattr(instance, "country", None)
|
||||
if country:
|
||||
country = country.code
|
||||
list_invalid.append(
|
||||
[
|
||||
tag,
|
||||
instance.id,
|
||||
_edit_url(tag, instance),
|
||||
instance.status,
|
||||
field_name,
|
||||
number,
|
||||
country,
|
||||
reason.strip(),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
"""
|
||||
Attempt to validate existing phone numbers to E164 format
|
||||
|
||||
Output any that can't be validated to a invalid_phonenumbers.csv file
|
||||
Output any that were fixed to a fixed_phonenumbers.csv file
|
||||
"""
|
||||
|
||||
InternetExchange = apps.get_model("peeringdb_server", "InternetExchange")
|
||||
NetworkContact = apps.get_model("peeringdb_server", "NetworkContact")
|
||||
|
||||
headers_invalid = [
|
||||
"type",
|
||||
"id",
|
||||
"status",
|
||||
"field",
|
||||
"phonenumber",
|
||||
"country",
|
||||
"reason",
|
||||
]
|
||||
|
||||
headers_fixed = [
|
||||
"type",
|
||||
"id",
|
||||
"status",
|
||||
"field",
|
||||
"phonenumber",
|
||||
"fixed",
|
||||
"country",
|
||||
]
|
||||
|
||||
invalid = []
|
||||
fixed = []
|
||||
|
||||
for ix in InternetExchange.handleref.filter(status__in=["ok", "pending"]):
|
||||
_fix_number("ix", ix, "tech_phone", fixed, invalid)
|
||||
_fix_number("ix", ix, "policy_phone", fixed, invalid)
|
||||
|
||||
for poc in NetworkContact.handleref.filter(status__in=["ok", "pending"]):
|
||||
_fix_number("poc", poc, "phone", fixed, invalid)
|
||||
|
||||
print(
|
||||
"Invalid numbers: {} - written to invalid_phonenumbers.csv".format(len(invalid))
|
||||
)
|
||||
|
||||
with open("invalid_phonenumbers.csv", "w+") as csvfile:
|
||||
csvwriter = csv.writer(csvfile, lineterminator="\n")
|
||||
csvwriter.writerow(headers_invalid)
|
||||
for row in invalid:
|
||||
csvwriter.writerow(row)
|
||||
|
||||
print("Fixed numbers: {} - written to fixed_phonenumbers.csv".format(len(fixed)))
|
||||
|
||||
with open("fixed_phonenumbers.csv", "w+") as csvfile:
|
||||
csvwriter = csv.writer(csvfile, lineterminator="\n")
|
||||
csvwriter.writerow(headers_fixed)
|
||||
for row in fixed:
|
||||
csvwriter.writerow(row)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("peeringdb_server", "0024_netixlan_asn"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards_func, migrations.RunPython.noop),
|
||||
]
|
||||
42
peeringdb_server/migrations/0026_help_text_228.py
Normal file
42
peeringdb_server/migrations/0026_help_text_228.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-12-11 14:51
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("peeringdb_server", "0025_E164_phonenumbers"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="network",
|
||||
name="info_prefixes4",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="Recommended IPv4 maximum-prefix limit to be configured on peering sessions for this ASN",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="network",
|
||||
name="info_prefixes6",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="Recommended IPv6 maximum-prefix limit to be configured on peering sessions for this ASN",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="network",
|
||||
name="irr_as_set",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Reference to an AS-SET or ROUTE-SET in Internet Routing Registry (IRR)",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
23
peeringdb_server/migrations/0027_never_via_route_servers.py
Normal file
23
peeringdb_server/migrations/0027_never_via_route_servers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.23 on 2019-12-11 16:33
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("peeringdb_server", "0026_help_text_228"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="network",
|
||||
name="info_never_via_route_servers",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Indicates if this network will announce its routes via rout servers or not",
|
||||
),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user